mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-02 13:44:16 +09:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f5a3e13e5 | ||
|
|
c50d5fc20d | ||
|
|
dfdb0438ad | ||
|
|
7868ade8f7 | ||
|
|
67904da6a5 | ||
|
|
6f5d66f6dd | ||
|
|
a1c56dd96a | ||
|
|
5a3f06430d | ||
|
|
6c8eaaf9e7 | ||
|
|
c9e1805cc9 | ||
|
|
5ba69f9af4 | ||
|
|
e395b4d9dc | ||
|
|
308dd93484 | ||
|
|
3162dc4cbf | ||
|
|
51736a9706 | ||
|
|
40079c7c09 | ||
|
|
2b11ba861f | ||
|
|
9ad9f99060 | ||
|
|
0964106988 | ||
|
|
8cf0f9798f | ||
|
|
5d737d660b | ||
|
|
5be73fa93a | ||
|
|
3ed5886066 | ||
|
|
12b5af068f | ||
|
|
8e83bb0e61 | ||
|
|
2042caf945 | ||
|
|
79189a959f | ||
|
|
1b8a7c4d85 | ||
|
|
dbe80ac05c | ||
|
|
4b669ca620 | ||
|
|
9d335e8783 | ||
|
|
85a85878c7 | ||
|
|
ef2ceff6cd | ||
|
|
35a6f3d4e0 | ||
|
|
cf3d325e45 | ||
|
|
b140311d54 | ||
|
|
2b0aa6cfd2 | ||
|
|
9d1d5ceddb | ||
|
|
9141a6386f | ||
|
|
a814cf7567 | ||
|
|
2fd12d5f76 | ||
|
|
640b528580 | ||
|
|
3e5256b6ca | ||
|
|
3996c2c520 | ||
|
|
716295015b | ||
|
|
f50a5d0dab | ||
|
|
bc3e228e78 | ||
|
|
193c676946 | ||
|
|
977c9181ad | ||
|
|
197e761471 | ||
|
|
01db50f0bf | ||
|
|
4b743d2e8c | ||
|
|
31653bdb34 | ||
|
|
7365946851 | ||
|
|
254e18107e | ||
|
|
290568a07b | ||
|
|
b758396619 | ||
|
|
9bc4ce88ee | ||
|
|
9bb9a51623 | ||
|
|
5838cc8172 |
No files matched your search
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- uses: go-gitea/giteabot@f48c6a15e0d384f037aea19cc05ff5e9551096b9 # v1.1.0
|
- uses: go-gitea/giteabot@36ea99f821fefa3e8fb26aeac6211735664cd9c0 # v1.3.1
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||||
gitea_fork: giteabot/gitea
|
gitea_fork: giteabot/gitea
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ on:
|
|||||||
checks:
|
checks:
|
||||||
description: Comma-separated list of non-backport checks to run
|
description: Comma-separated list of non-backport checks to run
|
||||||
required: false
|
required: false
|
||||||
default: labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions
|
default: backport_branches,labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
statuses: write
|
statuses: write
|
||||||
steps:
|
steps:
|
||||||
- uses: go-gitea/giteabot@8033358e118c0bd97f5e9902dc5d6633eadc6aeb # v1.2.1
|
- uses: go-gitea/giteabot@36ea99f821fefa3e8fb26aeac6211735664cd9c0 # v1.3.1
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
github_token: ${{ secrets.GITEABOT_TOKEN }}
|
||||||
checks: ${{ github.event.inputs.checks || 'labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
checks: ${{ github.event.inputs.checks || 'backport_branches,labels,merge_queue,lock,feedback,last_call,milestones,lgtm,translation_comment,pr_actions' }}
|
||||||
@@ -13,7 +13,9 @@ permissions: {}
|
|||||||
jobs:
|
jobs:
|
||||||
tag:
|
tag:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
environment: release-signing
|
environment:
|
||||||
|
name: release-signing
|
||||||
|
deployment: false
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func runListUsers(ctx context.Context, c *cli.Command) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
users, err := user_model.GetAllUsers(ctx)
|
users, err := user_model.GetAllUsersForAdminListing(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -41,17 +41,17 @@ func runListUsers(ctx context.Context, c *cli.Command) error {
|
|||||||
w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0)
|
||||||
|
|
||||||
if c.IsSet("admin") {
|
if c.IsSet("admin") {
|
||||||
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n")
|
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tType\n")
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
if u.IsAdmin {
|
if u.IsAdmin {
|
||||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive)
|
fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%s\n", u.ID, u.Name, u.Email, u.IsActive, u.Type.DisplayName())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
twofa := user_model.UserList(users).GetTwoFaStatus(ctx)
|
twofa := user_model.UserList(users).GetTwoFaStatus(ctx)
|
||||||
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\n")
|
fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\t2FA\tType\n")
|
||||||
for _, u := range users {
|
for _, u := range users {
|
||||||
fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID])
|
fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\t%t\t%s\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin, twofa[u.ID], u.Type.DisplayName())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -811,6 +811,7 @@ LEVEL = Info
|
|||||||
;[git.config]
|
;[git.config]
|
||||||
;diff.algorithm = histogram
|
;diff.algorithm = histogram
|
||||||
;core.logAllRefUpdates = true
|
;core.logAllRefUpdates = true
|
||||||
|
;maintenance.strategy = gc
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@@ -3083,7 +3084,7 @@ LEVEL = Info
|
|||||||
;LOG_RETENTION_DAYS = 365
|
;LOG_RETENTION_DAYS = 365
|
||||||
;; Days to keep artifacts. Old artifacts will be deleted after this period. 0 means keep forever.
|
;; Days to keep artifacts. Old artifacts will be deleted after this period. 0 means keep forever.
|
||||||
;; Changes only apply to newly uploaded artifacts, existing ones keep the expiry stored when they were uploaded.
|
;; Changes only apply to newly uploaded artifacts, existing ones keep the expiry stored when they were uploaded.
|
||||||
;; Artifacts could have their own retention periods by setting the `retention-days` option in `actions/upload-artifact` step.
|
;; Workflows can request a shorter retention period, down to 1 day.
|
||||||
;ARTIFACT_RETENTION_DAYS = 90
|
;ARTIFACT_RETENTION_DAYS = 90
|
||||||
;; Max artifact size in bytes that can be browsed or previewed in the web UI. Set to 0 to disable artifact previews, or -1 for no limit.
|
;; Max artifact size in bytes that can be browsed or previewed in the web UI. Set to 0 to disable artifact previews, or -1 for no limit.
|
||||||
;ARTIFACT_PREVIEW_MAX_SIZE = 10485760
|
;ARTIFACT_PREVIEW_MAX_SIZE = 10485760
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ module gitea.dev
|
|||||||
|
|
||||||
go 1.27
|
go 1.27
|
||||||
|
|
||||||
toolchain go1.27.2
|
toolchain go1.27.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
connectrpc.com/connect v1.21.0
|
connectrpc.com/connect v1.21.0
|
||||||
@@ -13,7 +13,7 @@ require (
|
|||||||
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
|
gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96
|
||||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
||||||
gitea.dev/actionslib v1.2.1
|
gitea.dev/actionslib v1.2.1
|
||||||
gitea.dev/sdk v1.2.0
|
gitea.dev/sdk v1.3.0
|
||||||
github.com/42wim/httpsig v1.2.4
|
github.com/42wim/httpsig v1.2.4
|
||||||
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432
|
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432
|
||||||
github.com/Azure/go-ntlmssp v0.1.1
|
github.com/Azure/go-ntlmssp v0.1.1
|
||||||
@@ -95,14 +95,14 @@ require (
|
|||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
gitlab.com/gitlab-org/api/client-go/v3 v3.13.0
|
gitlab.com/gitlab-org/api/client-go/v3 v3.13.0
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525
|
go.yaml.in/yaml/v4 v4.0.0-rc.6.0.20261013184558-04dd63131525
|
||||||
golang.org/x/crypto v0.57.0
|
golang.org/x/crypto v0.58.0
|
||||||
golang.org/x/image v0.46.0
|
golang.org/x/image v0.46.0
|
||||||
golang.org/x/mod v0.41.0
|
golang.org/x/mod v0.41.0
|
||||||
golang.org/x/net v0.60.0
|
golang.org/x/net v0.62.0
|
||||||
golang.org/x/oauth2 v0.37.0
|
golang.org/x/oauth2 v0.37.0
|
||||||
golang.org/x/sync v0.23.0
|
golang.org/x/sync v0.24.0
|
||||||
golang.org/x/sys v0.48.0
|
golang.org/x/sys v0.49.0
|
||||||
golang.org/x/text v0.42.0
|
golang.org/x/text v0.43.0
|
||||||
google.golang.org/grpc v1.84.0
|
google.golang.org/grpc v1.84.0
|
||||||
google.golang.org/protobuf v1.36.12
|
google.golang.org/protobuf v1.36.12
|
||||||
gopkg.in/ini.v1 v1.67.3
|
gopkg.in/ini.v1 v1.67.3
|
||||||
@@ -252,7 +252,7 @@ require (
|
|||||||
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||||
golang.org/x/time v0.15.0 // indirect
|
golang.org/x/time v0.15.0 // indirect
|
||||||
golang.org/x/tools v0.49.0 // indirect
|
golang.org/x/tools v0.51.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
modernc.org/libc v1.75.7 // indirect
|
modernc.org/libc v1.75.7 // indirect
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGq
|
|||||||
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
|
||||||
gitea.dev/actionslib v1.2.1 h1:GL//K/0zIZV6h1OOksv1awvFVg0rg7fug2xWcG7mkG0=
|
gitea.dev/actionslib v1.2.1 h1:GL//K/0zIZV6h1OOksv1awvFVg0rg7fug2xWcG7mkG0=
|
||||||
gitea.dev/actionslib v1.2.1/go.mod h1:1+gqOKGSEPn2IFHgY8S3GC5Ld+Hn8jF3OxiFHQ/fpx4=
|
gitea.dev/actionslib v1.2.1/go.mod h1:1+gqOKGSEPn2IFHgY8S3GC5Ld+Hn8jF3OxiFHQ/fpx4=
|
||||||
gitea.dev/sdk v1.2.0 h1:avRtJl/nKCGispgSalo9czoZM9Rto1awnE0caNAoXGo=
|
gitea.dev/sdk v1.3.0 h1:4U95+Ou6WHCQiRPDEyAzvl1Q7cxg5RUTWkqmOXr5w4I=
|
||||||
gitea.dev/sdk v1.2.0/go.mod h1:rfh5oNdIK24cbCREwIn1tqWKQW+IICXFGWJyebuOAOE=
|
gitea.dev/sdk v1.3.0/go.mod h1:85EBqSAqUn1KlFCk40y3sTjWjBsB/KouL3slGXMA/2o=
|
||||||
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
|
github.com/42wim/httpsig v1.2.4 h1:mI5bH0nm4xn7K18fo1K3okNDRq8CCJ0KbBYWyA6r8lU=
|
||||||
github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps=
|
github.com/42wim/httpsig v1.2.4/go.mod h1:yKsYfSyTBEohkPik224QPFylmzEBtda/kjyIAJjh3ps=
|
||||||
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 h1:3Fcz1QzlS7Jv4FT2KI3cHNSZL+KPN3dXxurn9f3YL/Y=
|
github.com/42wim/sshsig v0.0.0-20260317195500-b9f38cf0d432 h1:3Fcz1QzlS7Jv4FT2KI3cHNSZL+KPN3dXxurn9f3YL/Y=
|
||||||
@@ -686,8 +686,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
|
|||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||||
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
|
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||||
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
|
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||||
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ=
|
||||||
@@ -714,8 +714,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
|||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU=
|
golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY=
|
||||||
golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg=
|
golang.org/x/net v0.62.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||||
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98=
|
||||||
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -728,8 +728,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.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.7.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.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk=
|
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||||
golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -756,8 +756,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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.20.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.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -767,8 +767,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
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.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE=
|
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
|
||||||
golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc=
|
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
|
||||||
golang.org/x/text v0.3.0/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
@@ -779,8 +779,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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.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.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
|
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||||
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
|
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@@ -790,8 +790,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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
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.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||||
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
golang.org/x/tools v0.51.0 h1:k4Xc/1Om9jwkBJBo4NVLMSARBoWtK10mx+W5BnXCeAI=
|
||||||
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
golang.org/x/tools v0.51.0/go.mod h1:9eEncMayCV6zRMGhR5eZEC2iBx98qWcF1HZ9Z7wJOoA=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
@@ -93,7 +93,12 @@ const artifactKeepForever timeutil.TimeStamp = 0
|
|||||||
|
|
||||||
func artifactExpiry(requested optional.Option[timeutil.TimeStamp]) timeutil.TimeStamp {
|
func artifactExpiry(requested optional.Option[timeutil.TimeStamp]) timeutil.TimeStamp {
|
||||||
if requested.Has() {
|
if requested.Has() {
|
||||||
return max(requested.Value(), artifactKeepForever+1)
|
now := timeutil.TimeStampNow()
|
||||||
|
expiry := max(requested.Value(), now.Add(timeutil.Day)) // v4 clients use the runner's clock
|
||||||
|
if setting.Actions.ArtifactRetentionDays > 0 {
|
||||||
|
expiry = min(expiry, now.Add(timeutil.Day*setting.Actions.ArtifactRetentionDays))
|
||||||
|
}
|
||||||
|
return expiry
|
||||||
}
|
}
|
||||||
if setting.Actions.ArtifactRetentionDays <= 0 {
|
if setting.Actions.ArtifactRetentionDays <= 0 {
|
||||||
return artifactKeepForever
|
return artifactKeepForever
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ package actions
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.dev/modules/optional"
|
||||||
|
"gitea.dev/modules/setting"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
"gitea.dev/modules/timeutil"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@@ -25,3 +31,21 @@ func TestKeepLatestAttemptArtifacts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.Equal(t, []int64{1, 4, 5}, ids)
|
assert.Equal(t, []int64{1, 4, 5}, ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestArtifactExpiry(t *testing.T) {
|
||||||
|
defer timeutil.MockSet(time.Unix(1_000_000_000, 0))()
|
||||||
|
now := timeutil.TimeStampNow()
|
||||||
|
days := func(n int64) timeutil.TimeStamp { return now.Add(n * timeutil.Day) }
|
||||||
|
requested := optional.Some[timeutil.TimeStamp]
|
||||||
|
|
||||||
|
defer test.MockVariableValue(&setting.Actions.ArtifactRetentionDays, 90)()
|
||||||
|
assert.Equal(t, days(90), artifactExpiry(optional.None[timeutil.TimeStamp]()))
|
||||||
|
assert.Equal(t, days(5), artifactExpiry(requested(days(5))))
|
||||||
|
assert.Equal(t, days(1), artifactExpiry(requested(days(-2))), "runner clock behind must not expire the artifact at once")
|
||||||
|
assert.Equal(t, days(90), artifactExpiry(requested(days(1000))), "runner must not exceed the instance retention")
|
||||||
|
|
||||||
|
setting.Actions.ArtifactRetentionDays = 0
|
||||||
|
assert.Equal(t, artifactKeepForever, artifactExpiry(optional.None[timeutil.TimeStamp]()))
|
||||||
|
assert.Equal(t, days(1000), artifactExpiry(requested(days(1000))))
|
||||||
|
assert.Equal(t, days(1), artifactExpiry(requested(now)))
|
||||||
|
}
|
||||||
@@ -34,8 +34,8 @@ type ActionRun struct {
|
|||||||
RepoID int64 `xorm:"unique(repo_index) index(repo_status)"`
|
RepoID int64 `xorm:"unique(repo_index) index(repo_status)"`
|
||||||
Repo *repo_model.Repository `xorm:"-"`
|
Repo *repo_model.Repository `xorm:"-"`
|
||||||
OwnerID int64 `xorm:"index"`
|
OwnerID int64 `xorm:"index"`
|
||||||
WorkflowID string `xorm:"index"` // the name of workflow file
|
WorkflowID string `xorm:"index"` // the name of workflow file
|
||||||
Index int64 `xorm:"index unique(repo_index)"` // a unique number for each run of a repository
|
Index int64 `xorm:"unique(repo_index)"` // a unique number for each run of a repository
|
||||||
TriggerUserID int64 `xorm:"index"`
|
TriggerUserID int64 `xorm:"index"`
|
||||||
TriggerUser *user_model.User `xorm:"-"`
|
TriggerUser *user_model.User `xorm:"-"`
|
||||||
ScheduleID int64
|
ScheduleID int64
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import (
|
|||||||
|
|
||||||
// MaxJobNumPerRun is the maximum number of jobs in a single run.
|
// MaxJobNumPerRun is the maximum number of jobs in a single run.
|
||||||
// https://docs.github.com/en/actions/reference/limits#existing-system-limits
|
// https://docs.github.com/en/actions/reference/limits#existing-system-limits
|
||||||
// TODO: check this limit when creating jobs
|
|
||||||
const MaxJobNumPerRun = 256
|
const MaxJobNumPerRun = 256
|
||||||
|
|
||||||
// ActionRunJob represents a job of a run
|
// ActionRunJob represents a job of a run
|
||||||
@@ -100,7 +99,7 @@ type ActionRunJob struct {
|
|||||||
|
|
||||||
// IsReusableCaller marks this job as a reusable workflow caller.
|
// IsReusableCaller marks this job as a reusable workflow caller.
|
||||||
// Caller jobs do not run on a runner; their status is derived from their child jobs.
|
// Caller jobs do not run on a runner; their status is derived from their child jobs.
|
||||||
IsReusableCaller bool `xorm:"index NOT NULL DEFAULT FALSE"`
|
IsReusableCaller bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||||
// IsExpanded reports whether this job's lazy expansion (children-row insertion) is complete.
|
// IsExpanded reports whether this job's lazy expansion (children-row insertion) is complete.
|
||||||
// For a reusable workflow caller, true means children rows exist and CallPayload is populated.
|
// For a reusable workflow caller, true means children rows exist and CallPayload is populated.
|
||||||
IsExpanded bool `xorm:"NOT NULL DEFAULT FALSE"`
|
IsExpanded bool `xorm:"NOT NULL DEFAULT FALSE"`
|
||||||
@@ -440,14 +439,6 @@ func CollectAllDescendantJobs(parent *ActionRunJob, allJobs []*ActionRunJob) []*
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasWaitingJobsToPick reports whether any waiting, unclaimed, non-reusable job
|
|
||||||
// remains in the repo, i.e. work that an idle runner could still pick up.
|
|
||||||
func hasWaitingJobsToPick(ctx context.Context, repoID int64) (bool, error) {
|
|
||||||
return db.GetEngine(ctx).
|
|
||||||
Where("repo_id = ? AND task_id = ? AND status = ? AND is_reusable_caller = ?", repoID, 0, StatusWaiting, false).
|
|
||||||
Exist(&ActionRunJob{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, cols ...string) (int64, error) {
|
func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, cols ...string) (int64, error) {
|
||||||
e := db.GetEngine(ctx)
|
e := db.GetEngine(ctx)
|
||||||
|
|
||||||
@@ -498,7 +489,7 @@ func UpdateRunJob(ctx context.Context, job *ActionRunJob, cond builder.Cond, col
|
|||||||
// Only bump when waiting work actually remains for this repo, otherwise
|
// Only bump when waiting work actually remains for this repo, otherwise
|
||||||
// every job completion would needlessly bump the global version and wake
|
// every job completion would needlessly bump the global version and wake
|
||||||
// every idle runner instance-wide for nothing.
|
// every idle runner instance-wide for nothing.
|
||||||
hasWaiting, err := hasWaitingJobsToPick(ctx, job.RepoID)
|
hasWaiting, err := JobQueueOptions{RepoID: job.RepoID, Status: StatusWaiting}.session(ctx).Exist(new(ActionRunJob))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,13 +27,12 @@ func (opts JobQueueOptions) session(ctx context.Context) *xorm.Session {
|
|||||||
sess = sess.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID})
|
sess = sess.And(builder.Eq{"`action_run_job`.repo_id": opts.RepoID})
|
||||||
}
|
}
|
||||||
if opts.OwnerID > 0 {
|
if opts.OwnerID > 0 {
|
||||||
sess = sess.Join("INNER", "repository", "repository.id = `action_run_job`.repo_id AND repository.owner_id = ?", opts.OwnerID)
|
sess = sess.And(builder.In("`action_run_job`.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"owner_id": opts.OwnerID})))
|
||||||
}
|
}
|
||||||
return sess.And(opts.statusCond())
|
return sess.And(opts.statusCond())
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// keep in sync with CreateTaskForRunner
|
|
||||||
queuedJobsCond = builder.Eq{"`action_run_job`.status": StatusWaiting, "`action_run_job`.task_id": 0}
|
queuedJobsCond = builder.Eq{"`action_run_job`.status": StatusWaiting, "`action_run_job`.task_id": 0}
|
||||||
// a cancelling job still occupies its runner
|
// a cancelling job still occupies its runner
|
||||||
runningJobsCond = builder.In("`action_run_job`.status", StatusRunning, StatusCancelling)
|
runningJobsCond = builder.In("`action_run_job`.status", StatusRunning, StatusCancelling)
|
||||||
|
|||||||
+28
-32
@@ -261,21 +261,10 @@ var pickTaskBatchSize = 100
|
|||||||
// atomically claims it. It iterates through all matching jobs so that a
|
// atomically claims it. It iterates through all matching jobs so that a
|
||||||
// concurrent claim by another runner (which would lose the optimistic lock on
|
// concurrent claim by another runner (which would lose the optimistic lock on
|
||||||
// job #1) does not leave the remaining jobs permanently unassigned.
|
// job #1) does not leave the remaining jobs permanently unassigned.
|
||||||
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask, bool, error) {
|
func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (task *ActionTask, cancelledJobs []*ActionRunJob, err error) {
|
||||||
if db.InTransaction(ctx) {
|
if db.InTransaction(ctx) {
|
||||||
return nil, false, errors.New("CreateTaskForRunner must not be called within a database transaction")
|
return nil, nil, errors.New("CreateTaskForRunner must not be called within a database transaction")
|
||||||
}
|
}
|
||||||
e := db.GetEngine(ctx)
|
|
||||||
|
|
||||||
jobCond := builder.NewCond()
|
|
||||||
if runner.RepoID != 0 {
|
|
||||||
jobCond = builder.Eq{"repo_id": runner.RepoID}
|
|
||||||
} else if runner.OwnerID != 0 {
|
|
||||||
jobCond = builder.In("repo_id", builder.Select("`repository`.id").From("repository").
|
|
||||||
Join("INNER", "repo_unit", "`repository`.id = `repo_unit`.repo_id").
|
|
||||||
Where(builder.Eq{"`repository`.owner_id": runner.OwnerID, "`repo_unit`.type": unit.TypeActions}))
|
|
||||||
}
|
|
||||||
baseCond := builder.Eq{"task_id": 0, "status": StatusWaiting, "is_reusable_caller": false}.And(jobCond)
|
|
||||||
|
|
||||||
// TODO: a more efficient way to filter labels
|
// TODO: a more efficient way to filter labels
|
||||||
log.Trace("runner labels: %v", runner.AgentLabels)
|
log.Trace("runner labels: %v", runner.AgentLabels)
|
||||||
@@ -286,17 +275,17 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
var cursorUpdated timeutil.TimeStamp
|
var cursorUpdated timeutil.TimeStamp
|
||||||
var cursorID int64
|
var cursorID int64
|
||||||
for {
|
for {
|
||||||
cond := baseCond
|
sess := JobQueueOptions{RepoID: runner.RepoID, OwnerID: runner.OwnerID, Status: StatusWaiting}.session(ctx)
|
||||||
if cursorID > 0 {
|
if cursorID > 0 {
|
||||||
cond = cond.And(builder.Or(
|
sess.And(builder.Or(
|
||||||
builder.Gt{"updated": cursorUpdated},
|
builder.Gt{"updated": cursorUpdated},
|
||||||
builder.And(builder.Eq{"updated": cursorUpdated}, builder.Gt{"id": cursorID}),
|
builder.And(builder.Eq{"updated": cursorUpdated}, builder.Gt{"id": cursorID}),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
var jobs []*ActionRunJob
|
var jobs []*ActionRunJob
|
||||||
if err := e.Where(cond).Asc("updated", "id").Limit(pickTaskBatchSize).Find(&jobs); err != nil {
|
if err := sess.Asc("updated", "id").Limit(pickTaskBatchSize).Find(&jobs); err != nil {
|
||||||
return nil, false, err
|
return nil, cancelledJobs, err
|
||||||
}
|
}
|
||||||
// A short page means no waiting jobs remain beyond it.
|
// A short page means no waiting jobs remain beyond it.
|
||||||
isLastPage := len(jobs) < pickTaskBatchSize
|
isLastPage := len(jobs) < pickTaskBatchSize
|
||||||
@@ -309,28 +298,29 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
if !runner.CanMatchLabels(v.RunsOn) {
|
if !runner.CanMatchLabels(v.RunsOn) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
task, ok, err := claimJobForRunner(ctx, runner, v)
|
task, cancelled, err := claimJobForRunner(ctx, runner, v)
|
||||||
if err != nil {
|
if cancelled {
|
||||||
return nil, false, err
|
cancelledJobs = append(cancelledJobs, v)
|
||||||
}
|
}
|
||||||
if ok {
|
if err != nil || task != nil {
|
||||||
return task, true, nil
|
return task, cancelledJobs, err
|
||||||
}
|
}
|
||||||
// Another runner claimed this job concurrently; try the next one.
|
// Another runner claimed this job concurrently, or it was cancelled. Try the next one.
|
||||||
}
|
}
|
||||||
|
|
||||||
if isLastPage {
|
if isLastPage {
|
||||||
return nil, false, nil
|
return nil, cancelledJobs, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// claimJobForRunner attempts to atomically claim job for runner inside its own
|
// claimJobForRunner attempts to atomically claim job for runner inside its own
|
||||||
// transaction. Returns (task, true, nil) on success, or (nil, false, nil) when
|
// transaction. Returns the task on success, cancelled when the job's repository
|
||||||
// another runner wins the optimistic-lock race (the caller should try the next
|
// has Actions disabled, or neither when another runner wins the optimistic-lock
|
||||||
// candidate job).
|
// race (the caller should try the next candidate job).
|
||||||
func claimJobForRunner(ctx context.Context, runner *ActionRunner, job *ActionRunJob) (*ActionTask, bool, error) {
|
func claimJobForRunner(ctx context.Context, runner *ActionRunner, job *ActionRunJob) (*ActionTask, bool, error) {
|
||||||
var resultTask *ActionTask
|
var resultTask *ActionTask
|
||||||
|
var cancelled bool
|
||||||
|
|
||||||
err := db.WithTx(ctx, func(ctx context.Context) error {
|
err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
e := db.GetEngine(ctx)
|
e := db.GetEngine(ctx)
|
||||||
@@ -338,6 +328,15 @@ func claimJobForRunner(ctx context.Context, runner *ActionRunner, job *ActionRun
|
|||||||
if err := job.LoadAttributes(ctx); err != nil {
|
if err := job.LoadAttributes(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := job.Run.Repo.LoadUnits(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !job.Run.Repo.UnitEnabled(ctx, unit.TypeActions) {
|
||||||
|
job.Status, job.Stopped = StatusCancelled, timeutil.TimeStampNow()
|
||||||
|
affected, err := UpdateRunJob(ctx, job, builder.Eq{"task_id": 0, "status": StatusWaiting}, "status", "stopped")
|
||||||
|
cancelled = err == nil && affected == 1
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
now := timeutil.TimeStampNow()
|
now := timeutil.TimeStampNow()
|
||||||
job.Started = now
|
job.Started = now
|
||||||
@@ -403,13 +402,10 @@ func claimJobForRunner(ctx context.Context, runner *ActionRunner, job *ActionRun
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if errors.Is(err, errJobAlreadyClaimed) {
|
if err != nil && !errors.Is(err, errJobAlreadyClaimed) {
|
||||||
return nil, false, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
return resultTask, true, nil
|
return resultTask, cancelled, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReleaseTaskForRunner reverts a freshly-claimed but undelivered task: it deletes
|
// ReleaseTaskForRunner reverts a freshly-claimed but undelivered task: it deletes
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
runnerv1 "gitea.dev/actionslib/runner/v1"
|
runnerv1 "gitea.dev/actionslib/runner/v1"
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
|
"gitea.dev/models/unit"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
"gitea.dev/modules/actions/jobparser"
|
"gitea.dev/modules/actions/jobparser"
|
||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
@@ -258,6 +259,11 @@ func TestReleaseTaskForRunner(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, db.Insert(t.Context(), run))
|
require.NoError(t, db.Insert(t.Context(), run))
|
||||||
|
|
||||||
|
disabledRun := &ActionRun{RepoID: 2, TriggerUserID: 2}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), disabledRun))
|
||||||
|
disabledJob := &ActionRunJob{RunID: disabledRun.ID, RepoID: 2, Status: StatusWaiting, RunsOn: []string{"ubuntu-latest"}}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), disabledJob))
|
||||||
|
|
||||||
job := &ActionRunJob{
|
job := &ActionRunJob{
|
||||||
RunID: run.ID,
|
RunID: run.ID,
|
||||||
RepoID: run.RepoID,
|
RepoID: run.RepoID,
|
||||||
@@ -279,11 +285,13 @@ func TestReleaseTaskForRunner(t *testing.T) {
|
|||||||
}
|
}
|
||||||
runner.GenerateAndFillToken()
|
runner.GenerateAndFillToken()
|
||||||
require.NoError(t, db.Insert(t.Context(), runner))
|
require.NoError(t, db.Insert(t.Context(), runner))
|
||||||
|
require.NoError(t, db.Insert(t.Context(), &repo_model.RepoUnit{RepoID: run.RepoID, Type: unit.TypeActions}))
|
||||||
|
|
||||||
task, ok, err := CreateTaskForRunner(t.Context(), runner)
|
task, cancelledJobs, err := CreateTaskForRunner(t.Context(), runner)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, ok)
|
|
||||||
require.NotNil(t, task)
|
require.NotNil(t, task)
|
||||||
|
require.Len(t, cancelledJobs, 1)
|
||||||
|
assert.Equal(t, StatusCancelled, unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: disabledJob.ID}).Status)
|
||||||
|
|
||||||
claimed := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
|
claimed := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: job.ID})
|
||||||
require.Equal(t, StatusRunning, claimed.Status)
|
require.Equal(t, StatusRunning, claimed.Status)
|
||||||
@@ -324,6 +332,11 @@ func TestCreateTaskForRunnerPagination(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.NoError(t, db.Insert(t.Context(), run))
|
require.NoError(t, db.Insert(t.Context(), run))
|
||||||
|
|
||||||
|
disabledRun := &ActionRun{RepoID: 2, TriggerUserID: 2}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), disabledRun))
|
||||||
|
disabledJob := &ActionRunJob{RunID: disabledRun.ID, RepoID: 2, Status: StatusWaiting, RunsOn: []string{"ubuntu-latest"}}
|
||||||
|
require.NoError(t, db.Insert(t.Context(), disabledJob))
|
||||||
|
|
||||||
// Five waiting jobs the runner cannot run, then one it can.
|
// Five waiting jobs the runner cannot run, then one it can.
|
||||||
// With a page size of 2 the matching job only appears on the third page.
|
// With a page size of 2 the matching job only appears on the third page.
|
||||||
for i := range 5 {
|
for i := range 5 {
|
||||||
@@ -362,11 +375,13 @@ func TestCreateTaskForRunnerPagination(t *testing.T) {
|
|||||||
}
|
}
|
||||||
runner.GenerateAndFillToken()
|
runner.GenerateAndFillToken()
|
||||||
require.NoError(t, db.Insert(t.Context(), runner))
|
require.NoError(t, db.Insert(t.Context(), runner))
|
||||||
|
require.NoError(t, db.Insert(t.Context(), &repo_model.RepoUnit{RepoID: run.RepoID, Type: unit.TypeActions}))
|
||||||
|
|
||||||
task, ok, err := CreateTaskForRunner(t.Context(), runner)
|
task, cancelledJobs, err := CreateTaskForRunner(t.Context(), runner)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, ok)
|
|
||||||
require.NotNil(t, task)
|
require.NotNil(t, task)
|
||||||
|
require.Len(t, cancelledJobs, 1)
|
||||||
|
assert.Equal(t, StatusCancelled, unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: disabledJob.ID}).Status)
|
||||||
|
|
||||||
claimed := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: target.ID})
|
claimed := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: target.ID})
|
||||||
assert.Equal(t, StatusRunning, claimed.Status)
|
assert.Equal(t, StatusRunning, claimed.Status)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func (opts FindGPGKeyOptions) ToConds() builder.Cond {
|
|||||||
cond = cond.And(builder.Eq{"primary_key_id": ""})
|
cond = cond.And(builder.Eq{"primary_key_id": ""})
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.OwnerID > 0 {
|
if opts.OwnerID != 0 {
|
||||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||||
}
|
}
|
||||||
if opts.KeyID != "" {
|
if opts.KeyID != "" {
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ func (opts FindPublicKeyOptions) ToOrders() string {
|
|||||||
|
|
||||||
func (opts FindPublicKeyOptions) ToConds() builder.Cond {
|
func (opts FindPublicKeyOptions) ToConds() builder.Cond {
|
||||||
cond := builder.NewCond()
|
cond := builder.NewCond()
|
||||||
if opts.OwnerID > 0 {
|
if opts.OwnerID != 0 {
|
||||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||||
}
|
}
|
||||||
if opts.Fingerprint != "" {
|
if opts.Fingerprint != "" {
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (in
|
|||||||
|
|
||||||
func (status *CommitStatus) loadRepository(ctx context.Context) (err error) {
|
func (status *CommitStatus) loadRepository(ctx context.Context) (err error) {
|
||||||
if status.Repo == nil {
|
if status.Repo == nil {
|
||||||
status.Repo, err = repo_model.GetRepositoryByID(ctx, status.RepoID)
|
status.Repo, err = cache.GetWithContextCache(ctx, cachegroup.Repository, status.RepoID, repo_model.GetRepositoryByID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getRepositoryByID [%d]: %w", status.RepoID, err)
|
return fmt.Errorf("getRepositoryByID [%d]: %w", status.RepoID, err)
|
||||||
}
|
}
|
||||||
@@ -230,11 +230,9 @@ func (status *CommitStatus) cutTargetURLGiteaActionsPrefix(ctx context.Context)
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
if status.Repo == nil {
|
if err := status.loadRepository(ctx); err != nil {
|
||||||
if err := status.loadRepository(ctx); err != nil {
|
log.Error("loadRepository: %v", err)
|
||||||
log.Error("loadRepository: %v", err)
|
return "", false
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix := status.Repo.Link() + "/actions"
|
prefix := status.Repo.Link() + "/actions"
|
||||||
@@ -572,11 +570,12 @@ func SignCommitsApplyDoerPermission(ctx context.Context, doer *user_model.User,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func statusRepoCanReadActions(ctx context.Context, doer *user_model.User, status *CommitStatus) bool {
|
func statusRepoCanReadActions(ctx context.Context, doer *user_model.User, status *CommitStatus) bool {
|
||||||
|
if err := status.loadRepository(ctx); err != nil {
|
||||||
|
log.Error("loadRepository: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
perm, err := cache.GetWithContextCache(ctx, cachegroup.RepoUserPermission, access_model.RepoUserPermissionCacheKey(status.RepoID, doer),
|
perm, err := cache.GetWithContextCache(ctx, cachegroup.RepoUserPermission, access_model.RepoUserPermissionCacheKey(status.RepoID, doer),
|
||||||
func(ctx context.Context, _ string) (access_model.Permission, error) { // only runs on a cache miss
|
func(ctx context.Context, _key string) (access_model.Permission, error) {
|
||||||
if err := status.loadRepository(ctx); err != nil {
|
|
||||||
return access_model.Permission{}, err
|
|
||||||
}
|
|
||||||
return access_model.GetDoerRepoPermission(ctx, status.Repo, doer)
|
return access_model.GetDoerRepoPermission(ctx, status.Repo, doer)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
|
"gitea.dev/modules/cache"
|
||||||
"gitea.dev/modules/commitstatus"
|
"gitea.dev/modules/commitstatus"
|
||||||
"gitea.dev/modules/git"
|
"gitea.dev/modules/git"
|
||||||
|
|
||||||
@@ -281,8 +282,10 @@ func TestCommitStatusesApplyDoerPermission(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
git_model.CommitStatusesApplyDoerPermission(t.Context(), doer, statuses)
|
git_model.CommitStatusesApplyDoerPermission(cache.WithCacheContext(t.Context()), doer, statuses)
|
||||||
assert.Equal(t, visibleURL, statuses[0].TargetURL)
|
assert.Equal(t, visibleURL, statuses[0].TargetURL)
|
||||||
assert.Empty(t, statuses[1].TargetURL)
|
assert.Empty(t, statuses[1].TargetURL)
|
||||||
assert.Equal(t, "https://mycicd.org/1", statuses[2].TargetURL)
|
assert.Equal(t, "https://mycicd.org/1", statuses[2].TargetURL)
|
||||||
|
assert.NotNil(t, statuses[0].Repo)
|
||||||
|
assert.Same(t, statuses[0].Repo, statuses[2].Repo)
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,7 @@ import (
|
|||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrCommentNotExist represents a "CommentNotExist" kind of error.
|
// ErrCommentNotExist represents a "CommentNotExist" kind of error.
|
||||||
@@ -369,12 +370,19 @@ func (c *Comment) LoadIssue(ctx context.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ xorm.BeforeInsertProcessor = (*Comment)(nil)
|
||||||
|
_ xorm.BeforeUpdateProcessor = (*Comment)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
// BeforeInsert will be invoked by XORM before inserting a record
|
// BeforeInsert will be invoked by XORM before inserting a record
|
||||||
func (c *Comment) BeforeInsert() {
|
func (c *Comment) BeforeInsert() {
|
||||||
c.PatchQuoted = c.Patch
|
c.PatchQuoted = c.Patch
|
||||||
if !utf8.ValidString(c.Patch) {
|
if !utf8.ValidString(c.Patch) {
|
||||||
c.PatchQuoted = strconv.Quote(c.Patch)
|
c.PatchQuoted = strconv.Quote(c.Patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sanitizeDatabaseStringContent(&c.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeforeUpdate will be invoked by XORM before updating a record
|
// BeforeUpdate will be invoked by XORM before updating a record
|
||||||
@@ -383,6 +391,8 @@ func (c *Comment) BeforeUpdate() {
|
|||||||
if !utf8.ValidString(c.Patch) {
|
if !utf8.ValidString(c.Patch) {
|
||||||
c.PatchQuoted = strconv.Quote(c.Patch)
|
c.PatchQuoted = strconv.Quote(c.Patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sanitizeDatabaseStringContent(&c.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
|
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
|
||||||
@@ -1164,6 +1174,11 @@ func UpdateComment(ctx context.Context, c *Comment, contentVersion int, doer *us
|
|||||||
if affected == 0 {
|
if affected == 0 {
|
||||||
return ErrCommentAlreadyChanged
|
return ErrCommentAlreadyChanged
|
||||||
}
|
}
|
||||||
|
if c.Type == CommentTypeReview {
|
||||||
|
if _, err := db.GetEngine(ctx).ID(c.ReviewID).Cols("content").NoAutoTime().Update(&Review{Content: c.Content}); err != nil { // updated_unix picks a reviewer's latest review
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := c.LoadIssue(ctx); err != nil {
|
if err := c.LoadIssue(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-3
@@ -11,6 +11,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
project_model "gitea.dev/models/project"
|
project_model "gitea.dev/models/project"
|
||||||
@@ -25,6 +26,7 @@ import (
|
|||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrIssueNotExist represents a "IssueNotExist" kind of error.
|
// ErrIssueNotExist represents a "IssueNotExist" kind of error.
|
||||||
@@ -53,7 +55,7 @@ var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already
|
|||||||
// Issue represents an issue or pull request of repository.
|
// Issue represents an issue or pull request of repository.
|
||||||
type Issue struct {
|
type Issue struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
RepoID int64 `xorm:"INDEX UNIQUE(repo_index)"`
|
RepoID int64 `xorm:"INDEX(repo_closed_pull) UNIQUE(repo_index)"`
|
||||||
Repo *repo_model.Repository `xorm:"-"`
|
Repo *repo_model.Repository `xorm:"-"`
|
||||||
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
Index int64 `xorm:"UNIQUE(repo_index)"` // Index in one repository.
|
||||||
PosterID int64 `xorm:"INDEX"`
|
PosterID int64 `xorm:"INDEX"`
|
||||||
@@ -75,9 +77,9 @@ type Issue struct {
|
|||||||
AssigneeID int64 `xorm:"-"`
|
AssigneeID int64 `xorm:"-"`
|
||||||
Assignee *user_model.User `xorm:"-"`
|
Assignee *user_model.User `xorm:"-"`
|
||||||
isAssigneeLoaded bool `xorm:"-"`
|
isAssigneeLoaded bool `xorm:"-"`
|
||||||
IsClosed bool `xorm:"INDEX"`
|
IsClosed bool `xorm:"INDEX(repo_closed_pull)"`
|
||||||
IsRead bool `xorm:"-"`
|
IsRead bool `xorm:"-"`
|
||||||
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
|
IsPull bool `xorm:"INDEX(repo_closed_pull)"` // Indicates whether is a pull request or not.
|
||||||
PullRequest *PullRequest `xorm:"-"`
|
PullRequest *PullRequest `xorm:"-"`
|
||||||
NumComments int
|
NumComments int
|
||||||
|
|
||||||
@@ -123,6 +125,27 @@ func init() {
|
|||||||
db.RegisterModel(new(IssueIndex))
|
db.RegisterModel(new(IssueIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sanitizeDatabaseStringContent(s *string) {
|
||||||
|
// postgresql doesn't like "\x00"
|
||||||
|
// this is only a simple workaround for issues like https://github.com/go-gitea/gitea/issues/28890 and https://github.com/go-gitea/gitea/issues/40003
|
||||||
|
*s = strings.ReplaceAll(*s, "\x00", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ xorm.BeforeInsertProcessor = (*Issue)(nil)
|
||||||
|
_ xorm.BeforeUpdateProcessor = (*Issue)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (issue *Issue) BeforeInsert() {
|
||||||
|
sanitizeDatabaseStringContent(&issue.Title)
|
||||||
|
sanitizeDatabaseStringContent(&issue.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (issue *Issue) BeforeUpdate() {
|
||||||
|
sanitizeDatabaseStringContent(&issue.Title)
|
||||||
|
sanitizeDatabaseStringContent(&issue.Content)
|
||||||
|
}
|
||||||
|
|
||||||
// LoadTotalTimes load total tracked time
|
// LoadTotalTimes load total tracked time
|
||||||
func (issue *Issue) LoadTotalTimes(ctx context.Context) (err error) {
|
func (issue *Issue) LoadTotalTimes(ctx context.Context) (err error) {
|
||||||
opts := FindTrackedTimesOptions{IssueID: issue.ID}
|
opts := FindTrackedTimesOptions{IssueID: issue.ID}
|
||||||
|
|||||||
@@ -75,11 +75,11 @@ func applySorts(sess db.Session, sortType string, priorityRepoID int64) {
|
|||||||
// Since this sortType is dynamically created, it has to be treated specially.
|
// Since this sortType is dynamically created, it has to be treated specially.
|
||||||
if after, ok := strings.CutPrefix(sortType, ScopeSortPrefix); ok {
|
if after, ok := strings.CutPrefix(sortType, ScopeSortPrefix); ok {
|
||||||
scope := after
|
scope := after
|
||||||
sess.Join("LEFT", "issue_label", "issue.id = issue_label.issue_id")
|
// a subquery instead of a JOIN keeps one row per issue, "exclusive_order=0" means unset and COALESCE sorts it last on every DB
|
||||||
// "exclusive_order=0" means "no order is set", so exclude it from the JOIN criteria and then "LEFT JOIN" result is also null
|
sess.OrderBy("COALESCE((SELECT MIN(label.exclusive_order) FROM issue_label"+
|
||||||
sess.Join("LEFT", "label", "label.id = issue_label.label_id AND label.exclusive_order <> 0 AND label.name LIKE ?", scope+"/%")
|
" INNER JOIN label ON label.id = issue_label.label_id"+
|
||||||
// Use COALESCE to make sure we sort NULL last regardless of backend DB (2147483647 == max int)
|
" WHERE issue_label.issue_id = issue.id AND label.exclusive_order <> 0 AND label.name LIKE ?), 2147483647) ASC", scope+"/%").
|
||||||
sess.OrderBy("COALESCE(label.exclusive_order, 2147483647) ASC").Desc("issue.id")
|
Desc("issue.id")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,10 +218,7 @@ func applyRepoConditions(sess db.Session, opts *IssuesOptions) {
|
|||||||
opts.RepoCond = builder.In("issue.repo_id", opts.RepoIDs)
|
opts.RepoCond = builder.In("issue.repo_id", opts.RepoIDs)
|
||||||
}
|
}
|
||||||
if opts.AllPublic {
|
if opts.AllPublic {
|
||||||
if opts.RepoCond == nil {
|
opts.RepoCond = builder.Or(opts.RepoCond, repo_model.PublicRepoUnderPublicOwnerCond())
|
||||||
opts.RepoCond = builder.NewCond()
|
|
||||||
}
|
|
||||||
opts.RepoCond = opts.RepoCond.Or(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(repo_model.PublicRepoUnderPublicOwnerCond())))
|
|
||||||
}
|
}
|
||||||
if opts.RepoCond != nil {
|
if opts.RepoCond != nil {
|
||||||
sess.And(opts.RepoCond)
|
sess.And(opts.RepoCond)
|
||||||
@@ -402,22 +399,17 @@ func applyReviewRequestedCondition(sess db.Session, reviewRequestedID int64) {
|
|||||||
From("team_user").
|
From("team_user").
|
||||||
Where(builder.Eq{"team_user.uid": reviewRequestedID})
|
Where(builder.Eq{"team_user.uid": reviewRequestedID})
|
||||||
|
|
||||||
// if the review is approved or rejected, it should not be shown in the review requested list
|
|
||||||
maxReview := builder.Select("MAX(r.id)").
|
|
||||||
From("review as r").
|
|
||||||
Where(builder.In("r.type", []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest})).
|
|
||||||
GroupBy("r.issue_id, r.reviewer_id, r.reviewer_team_id")
|
|
||||||
|
|
||||||
subQuery := builder.Select("review.issue_id").
|
subQuery := builder.Select("review.issue_id").
|
||||||
From("review").
|
From("review").
|
||||||
Where(builder.And(
|
Where(builder.Eq{"review.type": ReviewTypeRequest}).
|
||||||
builder.Eq{"review.type": ReviewTypeRequest},
|
And(builder.Or(
|
||||||
builder.Or(
|
builder.Eq{"review.reviewer_id": reviewRequestedID},
|
||||||
builder.Eq{"review.reviewer_id": reviewRequestedID},
|
builder.In("review.reviewer_team_id", existInTeamQuery),
|
||||||
builder.In("review.reviewer_team_id", existInTeamQuery),
|
)).
|
||||||
),
|
// if the review is approved or rejected, it should not be shown in the review requested list
|
||||||
builder.In("review.id", maxReview),
|
And(builder.NotExists(builder.Select("1").From("review as r").
|
||||||
))
|
Where(builder.Expr("r.issue_id = review.issue_id AND r.reviewer_id = review.reviewer_id AND r.reviewer_team_id = review.reviewer_team_id AND r.id > review.id").
|
||||||
|
And(builder.In("r.type", []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest})))))
|
||||||
sess.Where("issue.poster_id <> ?", reviewRequestedID).
|
sess.Where("issue.poster_id <> ?", reviewRequestedID).
|
||||||
And(builder.In("issue.id", subQuery))
|
And(builder.In("issue.id", subQuery))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,12 +39,13 @@ func (milestones MilestoneList) SplitByOpenClosed() (open, closed MilestoneList)
|
|||||||
// FindMilestoneOptions contain options to get milestones
|
// FindMilestoneOptions contain options to get milestones
|
||||||
type FindMilestoneOptions struct {
|
type FindMilestoneOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
RepoID int64
|
RepoID int64
|
||||||
IsClosed optional.Option[bool]
|
IsClosed optional.Option[bool]
|
||||||
Name string
|
Name string
|
||||||
SortType string
|
SortType string
|
||||||
RepoCond builder.Cond
|
RepoCond builder.Cond
|
||||||
RepoIDs []int64
|
RepoIDs []int64
|
||||||
|
ProjectID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts FindMilestoneOptions) ToConds() builder.Cond {
|
func (opts FindMilestoneOptions) ToConds() builder.Cond {
|
||||||
@@ -64,6 +65,11 @@ func (opts FindMilestoneOptions) ToConds() builder.Cond {
|
|||||||
if len(opts.Name) != 0 {
|
if len(opts.Name) != 0 {
|
||||||
cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
|
cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
|
||||||
}
|
}
|
||||||
|
if opts.ProjectID != 0 {
|
||||||
|
cond = cond.And(builder.In("id", builder.Select("issue.milestone_id").From("issue").
|
||||||
|
InnerJoin("project_issue", "project_issue.issue_id = issue.id").
|
||||||
|
Where(builder.Eq{"project_issue.project_id": opts.ProjectID})))
|
||||||
|
}
|
||||||
|
|
||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ func (pr *PullRequest) getReviewedByLines(ctx context.Context, writer io.Writer)
|
|||||||
reviewersWritten := 0
|
reviewersWritten := 0
|
||||||
|
|
||||||
for _, review := range reviews {
|
for _, review := range reviews {
|
||||||
if maxReviewers > 0 && reviewersWritten > maxReviewers {
|
if maxReviewers > 0 && reviewersWritten >= maxReviewers {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -335,8 +335,8 @@ func (prs PullRequestList) LoadReviews(ctx context.Context) (ReviewList, error)
|
|||||||
"reviewer_team_id": 0,
|
"reviewer_team_id": 0,
|
||||||
}).
|
}).
|
||||||
GroupBy("issue_id, reviewer_id")
|
GroupBy("issue_id, reviewer_id")
|
||||||
// Get latest review of each reviewer, sorted in order they were made
|
// Get latest review of each reviewer, sorted in order they were made. Joined because MySQL scans all reviews for "id IN (subquery)", xorm aliases the subquery "review".
|
||||||
if err := db.GetEngine(ctx).In("id", subQuery).OrderBy("review.updated_unix ASC").Find(&reviews); err != nil {
|
if err := db.GetEngine(ctx).Table("review").Alias("latest").Join("INNER", subQuery, "latest.id = review.id").OrderBy("latest.updated_unix ASC").Find(&reviews); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +350,7 @@ func (prs PullRequestList) LoadReviews(ctx context.Context) (ReviewList, error)
|
|||||||
"reviewer_team_id": 0,
|
"reviewer_team_id": 0,
|
||||||
}).
|
}).
|
||||||
GroupBy("issue_id, reviewer_team_id")
|
GroupBy("issue_id, reviewer_team_id")
|
||||||
if err := db.GetEngine(ctx).In("id", subQueryTeam).OrderBy("review.updated_unix ASC").Find(&teamReviewRequests); err != nil {
|
if err := db.GetEngine(ctx).Table("review").Alias("latest").Join("INNER", subQueryTeam, "latest.id = review.id").OrderBy("latest.updated_unix ASC").Find(&teamReviewRequests); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"gitea.dev/models/unittest"
|
"gitea.dev/models/unittest"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -403,6 +404,10 @@ func testGetApprovers(t *testing.T) {
|
|||||||
approvers := pr.GetApprovers(t.Context())
|
approvers := pr.GetApprovers(t.Context())
|
||||||
expected := "Reviewed-by: User Five <user5@example.com>\nReviewed-by: Org Six <org6@example.com>\n"
|
expected := "Reviewed-by: User Five <user5@example.com>\nReviewed-by: Org Six <org6@example.com>\n"
|
||||||
assert.Equal(t, expected, approvers)
|
assert.Equal(t, expected, approvers)
|
||||||
|
|
||||||
|
defer test.MockVariableValue(&setting.Repository.PullRequest.DefaultMergeMessageMaxApprovers, 1)()
|
||||||
|
approvers = pr.GetApprovers(t.Context())
|
||||||
|
assert.Equal(t, "Reviewed-by: User Five <user5@example.com>\n", approvers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGetPullRequestByMergedCommit(t *testing.T) {
|
func testGetPullRequestByMergedCommit(t *testing.T) {
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ func queryUserOrgIDs(userID int64, includePrivate bool) *builder.Builder {
|
|||||||
|
|
||||||
func (opts FindOrgOptions) ToConds() builder.Cond {
|
func (opts FindOrgOptions) ToConds() builder.Cond {
|
||||||
var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization}
|
var cond builder.Cond = builder.Eq{"`user`.`type`": user_model.UserTypeOrganization}
|
||||||
if opts.UserID > 0 {
|
if opts.UserID != 0 {
|
||||||
cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludeVisibility == structs.VisibleTypePrivate)))
|
cond = cond.And(builder.In("`user`.`id`", queryUserOrgIDs(opts.UserID, opts.IncludeVisibility == structs.VisibleTypePrivate)))
|
||||||
}
|
}
|
||||||
// public=0, limited=1, private=2
|
// public=0, limited=1, private=2
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import (
|
|||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TeamRepo represents an team-repository relation.
|
// TeamRepo represents an org-team-repository relation.
|
||||||
type TeamRepo struct {
|
type TeamRepo struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
OrgID int64 `xorm:"INDEX"`
|
OrgID int64 `xorm:"INDEX"`
|
||||||
TeamID int64 `xorm:"UNIQUE(s)"`
|
TeamID int64 `xorm:"UNIQUE(s)"`
|
||||||
RepoID int64 `xorm:"UNIQUE(s)"`
|
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasTeamRepo returns true if given repository belongs to team.
|
// HasTeamRepo returns true if given repository belongs to team.
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import (
|
|||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TeamUser represents an team-user relation.
|
// TeamUser represents a team-user relation.
|
||||||
type TeamUser struct {
|
type TeamUser struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
OrgID int64 `xorm:"INDEX"`
|
OrgID int64 `xorm:"INDEX"`
|
||||||
TeamID int64 `xorm:"UNIQUE(s)"`
|
TeamID int64 `xorm:"UNIQUE(s)"`
|
||||||
UID int64 `xorm:"UNIQUE(s)"`
|
UID int64 `xorm:"UNIQUE(s) INDEX"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTeamMember returns true if given user is a member of team.
|
// IsTeamMember returns true if given user is a member of team.
|
||||||
|
|||||||
@@ -122,8 +122,8 @@ func GetTotalUnreferencedBlobSize(ctx context.Context) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsBlobAccessibleForUser tests if the user has access to the blob
|
// IsBlobAccessibleForUser tests if the user has access to the blob
|
||||||
func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model.User) (bool, error) {
|
func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model.User, publicOnly bool) (bool, error) {
|
||||||
if user.IsAdmin {
|
if user.IsAdmin && !publicOnly {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
ownerVisibilities := []structs.VisibleType{structs.VisibleTypePublic}
|
ownerVisibilities := []structs.VisibleType{structs.VisibleTypePublic}
|
||||||
@@ -153,6 +153,9 @@ func IsBlobAccessibleForUser(ctx context.Context, blobID int64, user *user_model
|
|||||||
Or(builder.Eq{"`user`.type": user_model.UserTypeOrganization}.
|
Or(builder.Eq{"`user`.type": user_model.UserTypeOrganization}.
|
||||||
And(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamAuthorize}.Or(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamUnitAccessMode}))),
|
And(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamAuthorize}.Or(builder.Lte{strconv.Itoa(int(perm.AccessModeRead)): maxTeamUnitAccessMode}))),
|
||||||
)
|
)
|
||||||
|
if publicOnly {
|
||||||
|
cond = cond.And(builder.Eq{"`user`.visibility": structs.VisibleTypePublic})
|
||||||
|
}
|
||||||
|
|
||||||
return db.GetEngine(ctx).
|
return db.GetEngine(ctx).
|
||||||
Table("package_blob").
|
Table("package_blob").
|
||||||
|
|||||||
@@ -64,11 +64,15 @@ func TestIsBlobAccessibleForRestrictedUser(t *testing.T) {
|
|||||||
_, err = TryInsertFile(t.Context(), &PackageFile{VersionID: version.ID, BlobID: blob.ID, Name: "blob", LowerName: "blob"})
|
_, err = TryInsertFile(t.Context(), &PackageFile{VersionID: version.ID, BlobID: blob.ID, Name: "blob", LowerName: "blob"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
accessible, err := IsBlobAccessibleForUser(t.Context(), blob.ID, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}))
|
accessible, err := IsBlobAccessibleForUser(t.Context(), blob.ID, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}), false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, accessible)
|
assert.True(t, accessible)
|
||||||
|
|
||||||
accessible, err = IsBlobAccessibleForUser(t.Context(), blob.ID, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}))
|
accessible, err = IsBlobAccessibleForUser(t.Context(), blob.ID, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29}), false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, accessible)
|
||||||
|
|
||||||
|
accessible, err = IsBlobAccessibleForUser(t.Context(), blob.ID, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}), true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, accessible)
|
assert.False(t, accessible)
|
||||||
}
|
}
|
||||||
@@ -30,9 +30,9 @@ const (
|
|||||||
// PackageProperty represents a property of a package, version or file
|
// PackageProperty represents a property of a package, version or file
|
||||||
type PackageProperty struct {
|
type PackageProperty struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
RefType PropertyType `xorm:"INDEX NOT NULL"`
|
RefType PropertyType `xorm:"NOT NULL"`
|
||||||
RefID int64 `xorm:"INDEX NOT NULL"`
|
RefID int64 `xorm:"INDEX(ref_id_name) NOT NULL"`
|
||||||
Name string `xorm:"INDEX NOT NULL"`
|
Name string `xorm:"INDEX(ref_id_name) NOT NULL"`
|
||||||
Value string `xorm:"LONGTEXT NOT NULL"`
|
Value string `xorm:"LONGTEXT NOT NULL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
type Access struct {
|
type Access struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
UserID int64 `xorm:"UNIQUE(s)"`
|
UserID int64 `xorm:"UNIQUE(s)"`
|
||||||
RepoID int64 `xorm:"UNIQUE(s)"`
|
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
|
||||||
Mode perm.AccessMode
|
Mode perm.AccessMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ func (opts SearchOptions) ToConds() builder.Cond {
|
|||||||
if opts.Type > 0 {
|
if opts.Type > 0 {
|
||||||
cond = cond.And(builder.Eq{"type": opts.Type})
|
cond = cond.And(builder.Eq{"type": opts.Type})
|
||||||
}
|
}
|
||||||
if opts.OwnerID > 0 {
|
if opts.OwnerID != 0 {
|
||||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -494,10 +494,9 @@ func (repo *Repository) composeCommonMetas(ctx context.Context) map[string]strin
|
|||||||
repo.MustOwner(ctx)
|
repo.MustOwner(ctx)
|
||||||
if repo.Owner.IsOrganization() {
|
if repo.Owner.IsOrganization() {
|
||||||
teams := make([]string, 0, 5)
|
teams := make([]string, 0, 5)
|
||||||
_ = db.GetEngine(ctx).Table("team_repo").
|
_ = db.GetEngine(ctx).Select("team.lower_name").Table("team_repo").
|
||||||
Join("INNER", "team", "team.id = team_repo.team_id").
|
Join("INNER", "team", "team.id = team_repo.team_id").
|
||||||
Where("team_repo.repo_id = ?", repo.ID).
|
Where("team_repo.repo_id = ?", repo.ID).
|
||||||
Select("team.lower_name").
|
|
||||||
OrderBy("team.lower_name").
|
OrderBy("team.lower_name").
|
||||||
Find(&teams)
|
Find(&teams)
|
||||||
metas["teams"] = "," + strings.Join(teams, ",") + ","
|
metas["teams"] = "," + strings.Join(teams, ",") + ","
|
||||||
|
|||||||
@@ -373,8 +373,7 @@ func SearchRepositoryCondition(opts SearchRepoOptions) builder.Cond {
|
|||||||
cond := builder.NewCond()
|
cond := builder.NewCond()
|
||||||
|
|
||||||
if opts.Private {
|
if opts.Private {
|
||||||
if opts.Actor != nil && !opts.Actor.IsAdmin && opts.Actor.ID != opts.OwnerID {
|
if opts.Actor != nil && !opts.Actor.IsAdmin {
|
||||||
// OK we're in the context of a User
|
|
||||||
cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
|
cond = cond.And(AccessibleRepositoryCondition(opts.Actor, unit.TypeInvalid))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -762,7 +761,8 @@ func FindUserCodeAccessibleOwnerRepoIDs(ctx context.Context, ownerID int64, user
|
|||||||
func PublicRepoUnderPublicOwnerCond() builder.Cond {
|
func PublicRepoUnderPublicOwnerCond() builder.Cond {
|
||||||
return builder.And(
|
return builder.And(
|
||||||
builder.Eq{"`repository`.is_private": false},
|
builder.Eq{"`repository`.is_private": false},
|
||||||
builder.In("`repository`.owner_id", builder.Select("id").From("`user`").Where(builder.Eq{"visibility": structs.VisibleTypePublic})),
|
builder.Exists(builder.Select("1").From("`user` repo_owner").
|
||||||
|
Where(builder.Expr("repo_owner.id = `repository`.owner_id").And(builder.Eq{"repo_owner.visibility": structs.VisibleTypePublic}))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func IsErrRepoAlreadyExist(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrRepoAlreadyExist) Error() string {
|
func (err ErrRepoAlreadyExist) Error() string {
|
||||||
return fmt.Sprintf("repository already exists [uname: %s, name: %s]", err.Uname, err.Name)
|
return fmt.Sprintf("repository already exists [owner: %s, repo: %s]", err.Uname, err.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrRepoAlreadyExist) Unwrap() error {
|
func (err ErrRepoAlreadyExist) Unwrap() error {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const (
|
|||||||
type Watch struct {
|
type Watch struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
UserID int64 `xorm:"UNIQUE(watch)"`
|
UserID int64 `xorm:"UNIQUE(watch)"`
|
||||||
RepoID int64 `xorm:"UNIQUE(watch)"`
|
RepoID int64 `xorm:"UNIQUE(watch) INDEX"`
|
||||||
Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
|
Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||||
@@ -112,6 +112,9 @@ func IsWatchingRepo(ctx context.Context, userID, repoID int64) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func watchRepoByMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) {
|
func watchRepoByMode(ctx context.Context, watch *Watch, mode WatchMode) (err error) {
|
||||||
|
if watch.UserID <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if watch.Mode == mode {
|
if watch.Mode == mode {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ func TestGetWatchers(t *testing.T) {
|
|||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
watches, err := repo_model.GetWatchers(t.Context(), repo.ID)
|
watches, err := repo_model.GetWatchers(t.Context(), repo.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// One watchers are inactive, thus minus 1
|
// One watcher is inactive, thus minus 1
|
||||||
assert.Len(t, watches, repo.NumWatches-1)
|
assert.Len(t, watches, repo.NumWatches-1)
|
||||||
for _, watch := range watches {
|
for _, watch := range watches {
|
||||||
assert.Equal(t, repo.ID, watch.RepoID)
|
assert.Equal(t, repo.ID, watch.RepoID)
|
||||||
@@ -90,24 +90,30 @@ func TestWatchIfAuto(t *testing.T) {
|
|||||||
|
|
||||||
setting.Service.AutoWatchOnChanges = true
|
setting.Service.AutoWatchOnChanges = true
|
||||||
|
|
||||||
// Must not add watch
|
// Must not add watch (user 8 has "don't watch" set)
|
||||||
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 8, 1, true))
|
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 8, 1, true))
|
||||||
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, watchers, prevCount)
|
assert.Len(t, watchers, prevCount)
|
||||||
|
|
||||||
// Should not add watch
|
// Should not add watch (read operation)
|
||||||
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 12, 1, false))
|
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 12, 1, false))
|
||||||
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, watchers, prevCount)
|
assert.Len(t, watchers, prevCount)
|
||||||
|
|
||||||
// Should add watch
|
// Should add watch (write operation)
|
||||||
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 12, 1, true))
|
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), 12, 1, true))
|
||||||
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, watchers, prevCount+1)
|
assert.Len(t, watchers, prevCount+1)
|
||||||
|
|
||||||
|
// Should not add watch (system user should not be able to watch)
|
||||||
|
assert.NoError(t, repo_model.WatchIfAuto(t.Context(), user_model.ActionsUserID, 1, true))
|
||||||
|
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, watchers, prevCount+1)
|
||||||
|
|
||||||
// Should remove watch, inhibit from adding auto
|
// Should remove watch, inhibit from adding auto
|
||||||
assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), user12, repo, false))
|
assert.NoError(t, repo_model.WatchRepoAuto(t.Context(), user12, repo, false))
|
||||||
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
watchers, err = repo_model.GetRepoWatchers(t.Context(), repo.ID, db.ListOptions{Page: 1})
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -103,6 +104,10 @@ func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveM
|
|||||||
require.NoError(t, err, "creating fixture %s", fixturePath)
|
require.NoError(t, err, "creating fixture %s", fixturePath)
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
_, err = fmt.Fprintf(out, "HTTP/1.1 %d %s\n", resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||||
|
require.NoError(t, err, "writing fixture header for %s", liveURL)
|
||||||
|
}
|
||||||
for _, name := range slices.Sorted(maps.Keys(resp.Header)) {
|
for _, name := range slices.Sorted(maps.Keys(resp.Header)) {
|
||||||
if slices.Contains(ignoredHeaders, strings.ToLower(name)) {
|
if slices.Contains(ignoredHeaders, strings.ToLower(name)) {
|
||||||
continue
|
continue
|
||||||
@@ -125,14 +130,21 @@ func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveM
|
|||||||
|
|
||||||
replayed := strings.ReplaceAll(string(raw), liveServerBaseURL, mockURL)
|
replayed := strings.ReplaceAll(string(raw), liveServerBaseURL, mockURL)
|
||||||
headers, body, _ := strings.Cut(replayed, "\n\n")
|
headers, body, _ := strings.Cut(replayed, "\n\n")
|
||||||
|
code := http.StatusOK
|
||||||
for line := range strings.SplitSeq(headers, "\n") {
|
for line := range strings.SplitSeq(headers, "\n") {
|
||||||
|
if strings.HasPrefix(line, "HTTP/") {
|
||||||
|
parts := strings.SplitN(line, " ", 3)
|
||||||
|
require.Len(t, parts, 3)
|
||||||
|
code, _ = strconv.Atoi(parts[1])
|
||||||
|
continue
|
||||||
|
}
|
||||||
name, value, ok := strings.Cut(line, ": ")
|
name, value, ok := strings.Cut(line, ": ")
|
||||||
if !ok || strings.EqualFold(name, "Content-Length") {
|
if !ok || strings.EqualFold(name, "Content-Length") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
w.Header().Set(name, value)
|
w.Header().Set(name, value)
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(code)
|
||||||
_, err = w.Write([]byte(body))
|
_, err = w.Write([]byte(body))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
"gitea.dev/modules/timeutil"
|
"gitea.dev/modules/timeutil"
|
||||||
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
@@ -32,6 +33,9 @@ func IsFollowing(ctx context.Context, userID, followID int64) bool {
|
|||||||
|
|
||||||
// FollowUser marks someone be another's follower.
|
// FollowUser marks someone be another's follower.
|
||||||
func FollowUser(ctx context.Context, user, follow *User) (err error) {
|
func FollowUser(ctx context.Context, user, follow *User) (err error) {
|
||||||
|
if !follow.IsFollowable() {
|
||||||
|
return util.NewInvalidArgumentErrorf("user %d can not be followed", follow.ID)
|
||||||
|
}
|
||||||
if user.ID == follow.ID || IsFollowing(ctx, user.ID, follow.ID) {
|
if user.ID == follow.ID || IsFollowing(ctx, user.ID, follow.ID) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-5
@@ -68,13 +68,20 @@ const (
|
|||||||
UserTypeRemoteUser // 5
|
UserTypeRemoteUser // 5
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AdminListedUserTypes HINT: USER-TYPE-GLOSSARY: administrators list every user type except organizations
|
||||||
|
var AdminListedUserTypes = []UserType{UserTypeIndividual, UserTypeUserReserved, UserTypeBot, UserTypeRemoteUser}
|
||||||
|
|
||||||
// DisplayName returns the English name of the user type for logs and the CLI, the UI translates "concept_user_*" instead
|
// DisplayName returns the English name of the user type for logs and the CLI, the UI translates "concept_user_*" instead
|
||||||
func (t UserType) DisplayName() string {
|
func (t UserType) DisplayName() string {
|
||||||
switch t {
|
switch t {
|
||||||
case UserTypeOrganization, UserTypeOrganizationReserved:
|
case UserTypeOrganization, UserTypeOrganizationReserved:
|
||||||
return "Organization"
|
return "Organization"
|
||||||
|
case UserTypeUserReserved:
|
||||||
|
return "Reserved"
|
||||||
case UserTypeBot:
|
case UserTypeBot:
|
||||||
return "Bot"
|
return "Bot"
|
||||||
|
case UserTypeRemoteUser:
|
||||||
|
return "Remote"
|
||||||
default:
|
default:
|
||||||
return "User"
|
return "User"
|
||||||
}
|
}
|
||||||
@@ -250,10 +257,10 @@ func (u *User) GetEmail() string {
|
|||||||
return u.Email
|
return u.Email
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllUsers returns a slice of all individual users found in DB.
|
// GetAllUsersForAdminListing returns a slice of all users of AdminListedUserTypes found in DB.
|
||||||
func GetAllUsers(ctx context.Context) ([]*User, error) {
|
func GetAllUsersForAdminListing(ctx context.Context) (users []*User, err error) {
|
||||||
users := make([]*User, 0)
|
err = db.GetEngine(ctx).OrderBy("id").In("type", AdminListedUserTypes).Find(&users)
|
||||||
return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).Find(&users)
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsLocal returns true if user login type is LoginPlain.
|
// IsLocal returns true if user login type is LoginPlain.
|
||||||
@@ -440,6 +447,18 @@ func (u *User) IsIndividual() bool {
|
|||||||
return u.ID > 0 && u.Type == UserTypeIndividual
|
return u.ID > 0 && u.Type == UserTypeIndividual
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsFollowable returns whether other users can follow this user (individuals, bots and organizations)
|
||||||
|
func (u *User) IsFollowable() bool {
|
||||||
|
if u.ID <= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch u.Type {
|
||||||
|
case UserTypeIndividual, UserTypeBot, UserTypeOrganization:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// IsTypeBot returns whether the user is of type bot
|
// IsTypeBot returns whether the user is of type bot
|
||||||
func (u *User) IsTypeBot() bool {
|
func (u *User) IsTypeBot() bool {
|
||||||
return u.Type == UserTypeBot
|
return u.Type == UserTypeBot
|
||||||
@@ -1048,7 +1067,7 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
|||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Glossaries for XxxUser:
|
// HINT: USER-TYPE-GLOSSARY: Glossaries for XxxUser:
|
||||||
// * Possible: The user may or may not exist in the database, but we can still return a valid user object (e.g. Ghost user) for the given ID.
|
// * Possible: The user may or may not exist in the database, but we can still return a valid user object (e.g. Ghost user) for the given ID.
|
||||||
// * Individual: The user is a real person, not an organization or a bot.
|
// * Individual: The user is a real person, not an organization or a bot.
|
||||||
// * Doer: The user is the one performing an action
|
// * Doer: The user is the one performing an action
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
actions_model "gitea.dev/models/actions"
|
actions_model "gitea.dev/models/actions"
|
||||||
"gitea.dev/models/db"
|
"gitea.dev/models/db"
|
||||||
git_model "gitea.dev/models/git"
|
git_model "gitea.dev/models/git"
|
||||||
repo_model "gitea.dev/models/repo"
|
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,19 +35,11 @@ func GetCommitActionsStatusMap(ctx context.Context, statuses []*git_model.Commit
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
statusByJobID := make(map[int64]*git_model.CommitStatus)
|
statusByJobID := make(map[int64]*git_model.CommitStatus)
|
||||||
repoByID := make(map[int64]*repo_model.Repository)
|
|
||||||
for _, status := range statuses {
|
for _, status := range statuses {
|
||||||
if status == nil || status.TargetURL == "" {
|
if status == nil || status.TargetURL == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if status.Repo == nil {
|
if _, jobID, ok := status.ParseGiteaActionsTargetURL(ctx); ok {
|
||||||
status.Repo = repoByID[status.RepoID]
|
|
||||||
}
|
|
||||||
// ParseGiteaActionsTargetURL lazy-loads status.Repo on miss; cache the
|
|
||||||
// outcome so later entries with the same RepoID skip that load.
|
|
||||||
_, jobID, ok := status.ParseGiteaActionsTargetURL(ctx)
|
|
||||||
repoByID[status.RepoID] = status.Repo
|
|
||||||
if ok {
|
|
||||||
statusByJobID[jobID] = status
|
statusByJobID[jobID] = status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ const (
|
|||||||
EmailAvatarLink = "email_avatar_link"
|
EmailAvatarLink = "email_avatar_link"
|
||||||
UserEmailAddresses = "user_email_addresses"
|
UserEmailAddresses = "user_email_addresses"
|
||||||
GPGKeyWithSubKeys = "gpg_key_with_subkeys"
|
GPGKeyWithSubKeys = "gpg_key_with_subkeys"
|
||||||
|
Repository = "repository"
|
||||||
RepoUserPermission = "repo_user_permission"
|
RepoUserPermission = "repo_user_permission"
|
||||||
)
|
)
|
||||||
@@ -52,7 +52,7 @@ func Lookup(label string) (e encoding.Encoding, name string) {
|
|||||||
|
|
||||||
type ConvertOpts struct {
|
type ConvertOpts struct {
|
||||||
KeepBOM bool
|
KeepBOM bool
|
||||||
ErrorReplacement []byte
|
ErrorReplacement []byte // leave it empty to remove the invalid bytes
|
||||||
ErrorReturnOrigin bool
|
ErrorReturnOrigin bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +99,12 @@ func ToUTF8DropErrors(content []byte) []byte {
|
|||||||
func ToUTF8(content []byte, opts ConvertOpts) []byte {
|
func ToUTF8(content []byte, opts ConvertOpts) []byte {
|
||||||
charsetLabel, _ := DetectEncoding(content)
|
charsetLabel, _ := DetectEncoding(content)
|
||||||
if charsetLabel == "UTF-8" {
|
if charsetLabel == "UTF-8" {
|
||||||
return maybeRemoveBOM(content, opts)
|
// even if the detected charset is UTF-8, the content may contain invalid UTF-8 sequences, so need to convert it to valid UTF-8
|
||||||
|
content = maybeRemoveBOM(content, opts)
|
||||||
|
if opts.ErrorReturnOrigin {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
return bytes.ToValidUTF8(content, opts.ErrorReplacement)
|
||||||
}
|
}
|
||||||
|
|
||||||
encoding, _ := Lookup(charsetLabel)
|
encoding, _ := Lookup(charsetLabel)
|
||||||
@@ -123,11 +128,7 @@ func ToUTF8(content []byte, opts ConvertOpts) []byte {
|
|||||||
if opts.ErrorReturnOrigin {
|
if opts.ErrorReturnOrigin {
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
if opts.ErrorReplacement == nil {
|
decoded = append(decoded, opts.ErrorReplacement...)
|
||||||
decoded = append(decoded, content[idx+n])
|
|
||||||
} else {
|
|
||||||
decoded = append(decoded, opts.ErrorReplacement...)
|
|
||||||
}
|
|
||||||
idx += n + 1
|
idx += n + 1
|
||||||
}
|
}
|
||||||
return maybeRemoveBOM(decoded, opts)
|
return maybeRemoveBOM(decoded, opts)
|
||||||
|
|||||||
@@ -171,6 +171,8 @@ func TestToUTF8DropErrors(t *testing.T) {
|
|||||||
|
|
||||||
res = ToUTF8DropErrors([]byte{0x00, 0x00, 0x00, 0x00})
|
res = ToUTF8DropErrors([]byte{0x00, 0x00, 0x00, 0x00})
|
||||||
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, res)
|
assert.Equal(t, []byte{0x00, 0x00, 0x00, 0x00}, res)
|
||||||
|
|
||||||
|
assert.Equal(t, "caf ", string(ToUTF8DropErrors([]byte("caf\xe9"))))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDetectEncoding(t *testing.T) {
|
func TestDetectEncoding(t *testing.T) {
|
||||||
|
|||||||
+13
-17
@@ -5,6 +5,7 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -200,22 +201,18 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
|
|||||||
}
|
}
|
||||||
if old {
|
if old {
|
||||||
begin, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
|
begin, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
|
||||||
end, _ = strconv.ParseInt(groups["endOld"], 10, 64)
|
end, _ = strconv.ParseInt(cmp.Or(groups["endOld"], "1"), 10, 64)
|
||||||
// init otherLine with begin of opposite side
|
// init otherLine with begin of opposite side
|
||||||
otherLine, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
|
otherLine, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
|
||||||
} else {
|
} else {
|
||||||
begin, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
|
begin, _ = strconv.ParseInt(groups["beginNew"], 10, 64)
|
||||||
if groups["endNew"] != "" {
|
end, _ = strconv.ParseInt(cmp.Or(groups["endNew"], "1"), 10, 64)
|
||||||
end, _ = strconv.ParseInt(groups["endNew"], 10, 64)
|
|
||||||
} else {
|
|
||||||
end = 0
|
|
||||||
}
|
|
||||||
// init otherLine with begin of opposite side
|
// init otherLine with begin of opposite side
|
||||||
otherLine, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
|
otherLine, _ = strconv.ParseInt(groups["beginOld"], 10, 64)
|
||||||
}
|
}
|
||||||
end += begin // end is for real only the number of lines in hunk
|
end += begin // end is for real only the number of lines in hunk
|
||||||
// lof is between begin and end
|
// lof is between begin and end
|
||||||
if begin <= line && end >= line {
|
if begin <= line && line < end {
|
||||||
hunk = append(hunk, lof)
|
hunk = append(hunk, lof)
|
||||||
currentLine = begin
|
currentLine = begin
|
||||||
continue
|
continue
|
||||||
@@ -265,27 +262,26 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
|
|||||||
oldBegin = otherLine
|
oldBegin = otherLine
|
||||||
newBegin = currentLine
|
newBegin = currentLine
|
||||||
}
|
}
|
||||||
// headers + hunk header
|
|
||||||
newHunk := make([]string, headerLines)
|
|
||||||
// transfer existing headers
|
|
||||||
copy(newHunk, hunk[:headerLines])
|
|
||||||
// transfer last n lines
|
|
||||||
newHunk = append(newHunk, hunk[len(hunk)-numbersOfLine-1:]...)
|
|
||||||
// calculate newBegin, ... by counting lines
|
// calculate newBegin, ... by counting lines
|
||||||
for i := len(hunk) - 1; i >= len(hunk)-numbersOfLine; i-- {
|
start := len(hunk)
|
||||||
switch hunk[i][0] {
|
for count := 0; count < numbersOfLine && start > headerLines+1; {
|
||||||
|
start--
|
||||||
|
switch hunk[start][0] {
|
||||||
case '+':
|
case '+':
|
||||||
newBegin--
|
newBegin--
|
||||||
newNumOfLines++
|
newNumOfLines++
|
||||||
case '-':
|
case '-':
|
||||||
oldBegin--
|
oldBegin--
|
||||||
oldNumOfLines++
|
oldNumOfLines++
|
||||||
|
case '\\':
|
||||||
|
continue
|
||||||
default:
|
default:
|
||||||
oldBegin--
|
oldBegin--
|
||||||
newBegin--
|
newBegin--
|
||||||
newNumOfLines++
|
newNumOfLines++
|
||||||
oldNumOfLines++
|
oldNumOfLines++
|
||||||
}
|
}
|
||||||
|
count++
|
||||||
}
|
}
|
||||||
|
|
||||||
// "git diff" outputs "@@ -1 +1,3 @@" for "OLD" => "A\nB\nC"
|
// "git diff" outputs "@@ -1 +1,3 @@" for "OLD" => "A\nB\nC"
|
||||||
@@ -294,9 +290,9 @@ func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLi
|
|||||||
// For example: "L1\nL2" => "A\nB", then the patch shows "L2" as line 1 on the left (deleted part)
|
// For example: "L1\nL2" => "A\nB", then the patch shows "L2" as line 1 on the left (deleted part)
|
||||||
|
|
||||||
// construct the new hunk header
|
// construct the new hunk header
|
||||||
newHunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
|
hunk[headerLines] = fmt.Sprintf("@@ -%d,%d +%d,%d @@",
|
||||||
oldBegin, oldNumOfLines, newBegin, newNumOfLines)
|
oldBegin, oldNumOfLines, newBegin, newNumOfLines)
|
||||||
return strings.Join(newHunk, "\n"), nil
|
return strings.Join(append(hunk[:headerLines+1], hunk[start:]...), "\n"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAffectedFiles returns the affected files between two commits
|
// GetAffectedFiles returns the affected files between two commits
|
||||||
|
|||||||
@@ -120,10 +120,18 @@ func TestCutDiffAroundLine(t *testing.T) {
|
|||||||
assert.Empty(t, emptyResult)
|
assert.Empty(t, emptyResult)
|
||||||
|
|
||||||
// Line is out of scope
|
// Line is out of scope
|
||||||
emptyResult, err = CutDiffAroundLine(strings.NewReader(exampleDiff), 434, false, 0)
|
emptyResult, err = CutDiffAroundLine(strings.NewReader(exampleDiff), 7, false, 3)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Empty(t, emptyResult)
|
assert.Empty(t, emptyResult)
|
||||||
|
|
||||||
|
noNewlineDiff := "diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1 +1 @@\n-a\n\\ No newline at end of file\n+a"
|
||||||
|
result, err = CutDiffAroundLine(strings.NewReader(noNewlineDiff), 1, false, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1,1 +1,1 @@\n-a\n\\ No newline at end of file\n+a", result)
|
||||||
|
result, err = CutDiffAroundLine(strings.NewReader(noNewlineDiff), 1, true, 2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "diff --git a/a b/a\n--- a/a\n+++ b/a\n@@ -1 +1 @@\n-a", result)
|
||||||
|
|
||||||
// Handle minus diffs properly
|
// Handle minus diffs properly
|
||||||
minusDiff, err := CutDiffAroundLine(strings.NewReader(breakingDiff), 2, false, 4)
|
minusDiff, err := CutDiffAroundLine(strings.NewReader(breakingDiff), 2, false, 4)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
+7
-3
@@ -71,12 +71,12 @@ func DefaultFeatures() *Features {
|
|||||||
func loadGitVersionFeatures() (*Features, error) {
|
func loadGitVersionFeatures() (*Features, error) {
|
||||||
stdout, _, runErr := gitcmd.NewCommand("version").RunStdString(context.Background())
|
stdout, _, runErr := gitcmd.NewCommand("version").RunStdString(context.Background())
|
||||||
if runErr != nil {
|
if runErr != nil {
|
||||||
return nil, runErr
|
return nil, fmt.Errorf(`failed to run command "git version": %w`, runErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
ver, err := parseGitVersionLine(strings.TrimSpace(stdout))
|
ver, err := parseGitVersionLine(strings.TrimSpace(stdout))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf(`failed to parse output of command "git version": %w`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
features := &Features{gitVersion: ver, UsingGogit: isGogit}
|
features := &Features{gitVersion: ver, UsingGogit: isGogit}
|
||||||
@@ -108,7 +108,11 @@ func parseGitVersionLine(s string) (*version.Version, error) {
|
|||||||
if len(versionFields) > 3 {
|
if len(versionFields) > 3 {
|
||||||
versionFields = versionFields[:3]
|
versionFields = versionFields[:3]
|
||||||
}
|
}
|
||||||
return version.NewVersion(strings.Join(versionFields, "."))
|
ver, err := version.NewVersion(strings.Join(versionFields, "."))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid git version: %q: %w", s, err)
|
||||||
|
}
|
||||||
|
return ver, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkGitVersionCompatibility(gitVer *version.Version) error {
|
func checkGitVersionCompatibility(gitVer *version.Version) error {
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ const (
|
|||||||
StderrPathSpec StderrPrefix = "fatal: pathspec"
|
StderrPathSpec StderrPrefix = "fatal: pathspec"
|
||||||
StderrBadRevision StderrPrefix = "fatal: bad revision"
|
StderrBadRevision StderrPrefix = "fatal: bad revision"
|
||||||
StderrNoSuchPath StderrPrefix = "fatal: no such path"
|
StderrNoSuchPath StderrPrefix = "fatal: no such path"
|
||||||
|
StderrBadObject StderrPrefix = "fatal: bad object "
|
||||||
|
|
||||||
StderrNoSuchRemote1 StderrPrefix = "fatal: no such remote" // git < 2.30, exit status 128
|
StderrNoSuchRemote1 StderrPrefix = "fatal: no such remote" // git < 2.30, exit status 128
|
||||||
StderrNoSuchRemote2 StderrPrefix = "error: no such remote" // git >= 2.30. exit status 2
|
StderrNoSuchRemote2 StderrPrefix = "error: no such remote" // git >= 2.30. exit status 2
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -60,11 +61,13 @@ func serveSetHeaderContentRelated(w http.ResponseWriter, contentType string) {
|
|||||||
header.Set("X-Content-Type-Options", "nosniff")
|
header.Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|
||||||
csp := serveHeaderCspDefault
|
csp := serveHeaderCspDefault
|
||||||
if strings.HasPrefix(contentType, "application/pdf") {
|
if mediaType, _, err := mime.ParseMediaType(contentType); err == nil { // browsers may parse malformed types differently
|
||||||
csp = serveHeaderCspPdf
|
if mediaType == "application/pdf" {
|
||||||
}
|
csp = serveHeaderCspPdf
|
||||||
if strings.HasPrefix(contentType, "video/") || strings.HasPrefix(contentType, "audio/") {
|
}
|
||||||
csp = serveHeaderCspAudioVideo
|
if strings.HasPrefix(mediaType, "video/") || strings.HasPrefix(mediaType, "audio/") {
|
||||||
|
csp = serveHeaderCspAudioVideo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if csp != "" {
|
if csp != "" {
|
||||||
header.Set("Content-Security-Policy", csp)
|
header.Set("Content-Security-Policy", csp)
|
||||||
|
|||||||
@@ -117,9 +117,10 @@ func TestServeSetHeaderContentRelated(t *testing.T) {
|
|||||||
{"", serveHeaderCspDefault},
|
{"", serveHeaderCspDefault},
|
||||||
{"any", serveHeaderCspDefault},
|
{"any", serveHeaderCspDefault},
|
||||||
{"application/pdf", serveHeaderCspPdf},
|
{"application/pdf", serveHeaderCspPdf},
|
||||||
{"application/pdf; other", serveHeaderCspPdf},
|
{"application/pdf; charset=binary", serveHeaderCspPdf},
|
||||||
{"audio/mp4", serveHeaderCspAudioVideo},
|
{"audio/mp4", serveHeaderCspAudioVideo},
|
||||||
{"video/ogg; other", serveHeaderCspAudioVideo},
|
{"video/ogg; codecs=theora", serveHeaderCspAudioVideo},
|
||||||
|
{"video/mp4;,text/html", serveHeaderCspDefault},
|
||||||
{typesniffer.MimeTypeImageSvg, serveHeaderCspDefault},
|
{typesniffer.MimeTypeImageSvg, serveHeaderCspDefault},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type Decoder interface {
|
|||||||
// Interface represents an interface to handle json data
|
// Interface represents an interface to handle json data
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
Marshal(v any) ([]byte, error)
|
Marshal(v any) ([]byte, error)
|
||||||
MarshalWrite(w io.Writer, v any) error
|
MarshalWriteRelaxed(w io.Writer, v any) error
|
||||||
Unmarshal(data []byte, v any) error
|
Unmarshal(data []byte, v any) error
|
||||||
NewEncoder(writer io.Writer) Encoder
|
NewEncoder(writer io.Writer) Encoder
|
||||||
NewDecoder(reader io.Reader) Decoder
|
NewDecoder(reader io.Reader) Decoder
|
||||||
@@ -37,9 +37,10 @@ func Marshal(v any) ([]byte, error) {
|
|||||||
return DefaultJSONHandler.Marshal(v)
|
return DefaultJSONHandler.Marshal(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalWrite writes the JSON encoding of v to the given writer
|
// MarshalWriteRelaxed writes the JSON encoding of v to the given writer
|
||||||
func MarshalWrite(w io.Writer, v any) error {
|
// It tolerates invalid UTF-8 in strings and replaces them with the Unicode replacement character U+FFFD.
|
||||||
return DefaultJSONHandler.MarshalWrite(w, v)
|
func MarshalWriteRelaxed(w io.Writer, v any) error {
|
||||||
|
return DefaultJSONHandler.MarshalWriteRelaxed(w, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal decodes object from bytes
|
// Unmarshal decodes object from bytes
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
type JSONv2 struct {
|
type JSONv2 struct {
|
||||||
marshalOptions jsonv2.Options
|
marshalOptions jsonv2.Options
|
||||||
|
marshalRelaxedOptions jsonv2.Options
|
||||||
marshalKeepOptionalEmptyOptions jsonv2.Options
|
marshalKeepOptionalEmptyOptions jsonv2.Options
|
||||||
unmarshalOptions jsonv2.Options
|
unmarshalOptions jsonv2.Options
|
||||||
unmarshalCaseInsensitiveOptions jsonv2.Options
|
unmarshalCaseInsensitiveOptions jsonv2.Options
|
||||||
@@ -26,6 +27,8 @@ func init() {
|
|||||||
jsonv2.FormatNilMapAsNull(true),
|
jsonv2.FormatNilMapAsNull(true),
|
||||||
}
|
}
|
||||||
jsonV2.marshalOptions = jsonv2.JoinOptions(commonMarshalOptions...)
|
jsonV2.marshalOptions = jsonv2.JoinOptions(commonMarshalOptions...)
|
||||||
|
// when handling user's data, we want to allow invalid UTF-8 to be replaced with the replacement character, rather than returning an error.
|
||||||
|
jsonV2.marshalRelaxedOptions = jsonv2.JoinOptions(append(commonMarshalOptions, jsontext.AllowInvalidUTF8(true))...)
|
||||||
jsonV2.unmarshalOptions = jsonv2.DefaultOptionsV2()
|
jsonV2.unmarshalOptions = jsonv2.DefaultOptionsV2()
|
||||||
|
|
||||||
// By default, "json/v2" omitempty removes all `""` empty strings, no matter where it comes from.
|
// By default, "json/v2" omitempty removes all `""` empty strings, no matter where it comes from.
|
||||||
@@ -53,8 +56,8 @@ func (j *JSONv2) Marshal(v any) ([]byte, error) {
|
|||||||
return jsonv2.Marshal(v, j.marshalOptions)
|
return jsonv2.Marshal(v, j.marshalOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *JSONv2) MarshalWrite(w io.Writer, v any) error {
|
func (j *JSONv2) MarshalWriteRelaxed(w io.Writer, v any) error {
|
||||||
return jsonv2.MarshalWrite(w, v, j.marshalOptions)
|
return jsonv2.MarshalWrite(w, v, j.marshalRelaxedOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *JSONv2) Unmarshal(data []byte, v any) error {
|
func (j *JSONv2) Unmarshal(data []byte, v any) error {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type UploadCallback func(p Pointer, objectError error) (io.ReadCloser, error)
|
|||||||
// Client is used to communicate with a LFS source
|
// Client is used to communicate with a LFS source
|
||||||
type Client interface {
|
type Client interface {
|
||||||
BatchSize() int
|
BatchSize() int
|
||||||
Download(ctx context.Context, objects []Pointer, callback DownloadCallback) error
|
Download(ctx context.Context, ref string, objects []Pointer, callback DownloadCallback) error // ref is the full git ref name the objects belong to, may be empty
|
||||||
Upload(ctx context.Context, objects []Pointer, callback UploadCallback) error
|
Upload(ctx context.Context, objects []Pointer, callback UploadCallback) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func (c *FilesystemClient) objectPath(oid string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Download reads the specific LFS object from the target path
|
// Download reads the specific LFS object from the target path
|
||||||
func (c *FilesystemClient) Download(ctx context.Context, objects []Pointer, callback DownloadCallback) error {
|
func (c *FilesystemClient) Download(ctx context.Context, _ string, objects []Pointer, callback DownloadCallback) error {
|
||||||
for _, object := range objects {
|
for _, object := range objects {
|
||||||
p := Pointer{object.Oid, object.Size}
|
p := Pointer{object.Oid, object.Size}
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -67,18 +67,18 @@ func (c *HTTPClient) transferNames() []string {
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *HTTPClient) batch(ctx context.Context, operation string, objects []Pointer) (*BatchResponse, error) {
|
func (c *HTTPClient) batch(ctx context.Context, operation, ref string, objects []Pointer) (*BatchResponse, error) {
|
||||||
log.Trace("BATCH operation with objects: %v", objects)
|
log.Trace("BATCH operation with objects: %v", objects)
|
||||||
|
|
||||||
url := c.endpoint + "/objects/batch"
|
url := c.endpoint + "/objects/batch"
|
||||||
|
|
||||||
// Original: In some lfs server implementations, they require the ref attribute. #32838
|
// `ref` is optional, but some servers like aliyun require it, while others like Hugging Face reject an empty or null one
|
||||||
// `ref` is an "optional object describing the server ref that the objects belong to"
|
|
||||||
// but some (incorrect) lfs servers like aliyun require it, so maybe adding an empty ref here doesn't break the correct ones.
|
|
||||||
// https://github.com/git-lfs/git-lfs/blob/a32a02b44bf8a511aa14f047627c49e1a7fd5021/docs/api/batch.md?plain=1#L37
|
// https://github.com/git-lfs/git-lfs/blob/a32a02b44bf8a511aa14f047627c49e1a7fd5021/docs/api/batch.md?plain=1#L37
|
||||||
//
|
// Related issues: #32838, #33453, #39891
|
||||||
// UPDATE: it can't use "empty ref" here because it breaks others like https://github.com/go-gitea/gitea/issues/33453
|
request := &BatchRequest{Operation: operation, Transfers: c.transferNames(), Objects: objects}
|
||||||
request := &BatchRequest{operation, c.transferNames(), nil, objects}
|
if ref != "" {
|
||||||
|
request.Ref = &Reference{Name: ref}
|
||||||
|
}
|
||||||
|
|
||||||
payload := new(bytes.Buffer)
|
payload := new(bytes.Buffer)
|
||||||
err := json.NewEncoder(payload).Encode(request)
|
err := json.NewEncoder(payload).Encode(request)
|
||||||
@@ -113,17 +113,17 @@ func (c *HTTPClient) batch(ctx context.Context, operation string, objects []Poin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Download reads the specific LFS object from the LFS server
|
// Download reads the specific LFS object from the LFS server
|
||||||
func (c *HTTPClient) Download(ctx context.Context, objects []Pointer, callback DownloadCallback) error {
|
func (c *HTTPClient) Download(ctx context.Context, ref string, objects []Pointer, callback DownloadCallback) error {
|
||||||
return c.performOperation(ctx, objects, callback, nil)
|
return c.performOperation(ctx, ref, objects, callback, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload sends the specific LFS object to the LFS server
|
// Upload sends the specific LFS object to the LFS server
|
||||||
func (c *HTTPClient) Upload(ctx context.Context, objects []Pointer, callback UploadCallback) error {
|
func (c *HTTPClient) Upload(ctx context.Context, objects []Pointer, callback UploadCallback) error {
|
||||||
return c.performOperation(ctx, objects, nil, callback)
|
return c.performOperation(ctx, "", objects, nil, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
// performOperation takes a slice of LFS object pointers, batches them, and performs the upload/download operations concurrently in each batch
|
// performOperation takes a slice of LFS object pointers, batches them, and performs the upload/download operations concurrently in each batch
|
||||||
func (c *HTTPClient) performOperation(ctx context.Context, objects []Pointer, dc DownloadCallback, uc UploadCallback) error {
|
func (c *HTTPClient) performOperation(ctx context.Context, ref string, objects []Pointer, dc DownloadCallback, uc UploadCallback) error {
|
||||||
if len(objects) == 0 {
|
if len(objects) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -133,7 +133,7 @@ func (c *HTTPClient) performOperation(ctx context.Context, objects []Pointer, dc
|
|||||||
operation = "upload"
|
operation = "upload"
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := c.batch(ctx, operation, objects)
|
result, err := c.batch(ctx, operation, ref, objects)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ func TestHTTPClientDownload(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "download", batchRequest.Operation)
|
assert.Equal(t, "download", batchRequest.Operation)
|
||||||
|
assert.Equal(t, &Reference{Name: "refs/heads/main"}, batchRequest.Ref)
|
||||||
assert.Len(t, batchRequest.Objects, 1)
|
assert.Len(t, batchRequest.Objects, 1)
|
||||||
assert.Equal(t, p.Oid, batchRequest.Objects[0].Oid)
|
assert.Equal(t, p.Oid, batchRequest.Objects[0].Oid)
|
||||||
assert.Equal(t, p.Size, batchRequest.Objects[0].Size)
|
assert.Equal(t, p.Size, batchRequest.Objects[0].Size)
|
||||||
@@ -248,7 +249,7 @@ func TestHTTPClientDownload(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := client.Download(t.Context(), []Pointer{p}, func(p Pointer, content io.ReadCloser, objectError error) error {
|
err := client.Download(t.Context(), "refs/heads/main", []Pointer{p}, func(p Pointer, content io.ReadCloser, objectError error) error {
|
||||||
if objectError != nil {
|
if objectError != nil {
|
||||||
return objectError
|
return objectError
|
||||||
}
|
}
|
||||||
@@ -283,6 +284,7 @@ func TestHTTPClientUpload(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "upload", batchRequest.Operation)
|
assert.Equal(t, "upload", batchRequest.Operation)
|
||||||
|
assert.Nil(t, batchRequest.Ref)
|
||||||
assert.Len(t, batchRequest.Objects, 1)
|
assert.Len(t, batchRequest.Objects, 1)
|
||||||
assert.Equal(t, p.Oid, batchRequest.Objects[0].Oid)
|
assert.Equal(t, p.Oid, batchRequest.Objects[0].Oid)
|
||||||
assert.Equal(t, p.Size, batchRequest.Objects[0].Size)
|
assert.Equal(t, p.Size, batchRequest.Objects[0].Size)
|
||||||
|
|||||||
@@ -153,8 +153,6 @@ func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content st
|
|||||||
issueIndexPatternProcessor,
|
issueIndexPatternProcessor,
|
||||||
commitCrossReferencePatternProcessor,
|
commitCrossReferencePatternProcessor,
|
||||||
hashCurrentPatternProcessor,
|
hashCurrentPatternProcessor,
|
||||||
emojiShortCodeProcessor,
|
|
||||||
emojiProcessor,
|
|
||||||
linkProcessor,
|
linkProcessor,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +173,9 @@ func PostProcessCommitMessageSubject(ctx *RenderContext, defaultLink, content st
|
|||||||
node.DataAtom = atom.A
|
node.DataAtom = atom.A
|
||||||
node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted title-full-link"}}
|
node.Attr = []html.Attribute{{Key: "href", Val: defaultLink}, {Key: "class", Val: "muted title-full-link"}}
|
||||||
node.FirstChild, node.LastChild = ch, ch
|
node.FirstChild, node.LastChild = ch, ch
|
||||||
|
for n := node.FirstChild; n != nil; {
|
||||||
|
n = visitNode(ctx, emojiProcessors, n)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
rendered := postProcessHTML(ctx, procs, htmlutil.EscapeString(content))
|
rendered := postProcessHTML(ctx, procs, htmlutil.EscapeString(content))
|
||||||
return htmlutil.HTMLFormat(`<span class="title-full-link-hover">%s</span>`, rendered)
|
return htmlutil.HTMLFormat(`<span class="title-full-link-hover">%s</span>`, rendered)
|
||||||
|
|||||||
@@ -4,17 +4,28 @@
|
|||||||
package optional
|
package optional
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
|
|
||||||
"gitea.dev/modules/json"
|
"gitea.dev/modules/json"
|
||||||
|
|
||||||
"go.yaml.in/yaml/v4"
|
"go.yaml.in/yaml/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (o *Option[T]) assignUnmarshalledValue(v *T) {
|
||||||
|
if reflect.TypeFor[T]().Kind() == reflect.Pointer && v == nil {
|
||||||
|
var zero T
|
||||||
|
*o = Some[T](zero)
|
||||||
|
} else {
|
||||||
|
*o = FromPtr(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (o *Option[T]) UnmarshalJSON(data []byte) error {
|
func (o *Option[T]) UnmarshalJSON(data []byte) error {
|
||||||
var v *T
|
var v *T
|
||||||
if err := json.Unmarshal(data, &v); err != nil {
|
if err := json.Unmarshal(data, &v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
*o = FromPtr(v)
|
o.assignUnmarshalledValue(v)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +42,7 @@ func (o *Option[T]) UnmarshalYAML(value *yaml.Node) error {
|
|||||||
if err := value.Decode(&v); err != nil {
|
if err := value.Decode(&v); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
*o = FromPtr(v)
|
o.assignUnmarshalledValue(v)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package optional_test
|
|||||||
import (
|
import (
|
||||||
std_json "encoding/json" //nolint:depguard // for testing purpose
|
std_json "encoding/json" //nolint:depguard // for testing purpose
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"gitea.dev/modules/json"
|
"gitea.dev/modules/json"
|
||||||
"gitea.dev/modules/optional"
|
"gitea.dev/modules/optional"
|
||||||
@@ -28,6 +29,14 @@ type testSerializationStruct struct {
|
|||||||
OptTwoString optional.Option[string] `json:"optional_two_string" yaml:"optional_two_string"`
|
OptTwoString optional.Option[string] `json:"optional_two_string" yaml:"optional_two_string"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testSerializationNullValue struct {
|
||||||
|
OptString optional.Option[string] `json:"opt_null,omitempty" yaml:"opt_null,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type testSerializationNullPointer struct {
|
||||||
|
OptNull optional.Option[*time.Time] `json:"opt_null,omitempty" yaml:"opt_null,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func TestOptionalToJson(t *testing.T) {
|
func TestOptionalToJson(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -63,6 +72,32 @@ func TestOptionalToJson(t *testing.T) {
|
|||||||
assert.Equal(t, tc.want, string(b), "std json module returned unexpected")
|
assert.Equal(t, tc.want, string(b), "std json module returned unexpected")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Run("NullValue", func(t *testing.T) {
|
||||||
|
var v1 testSerializationNullValue
|
||||||
|
err := json.Unmarshal([]byte(`{"opt_null":null}`), &v1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, v1.OptString.Has())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NullPointer", func(t *testing.T) {
|
||||||
|
var v1 testSerializationNullPointer
|
||||||
|
err := json.Unmarshal([]byte(`{"opt_null":null}`), &v1)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, v1.OptNull.Has())
|
||||||
|
assert.Nil(t, v1.OptNull.Value())
|
||||||
|
|
||||||
|
var v2 testSerializationNullPointer
|
||||||
|
err = std_json.Unmarshal([]byte(`{}`), &v2)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, v2.OptNull.Has())
|
||||||
|
|
||||||
|
var v3 testSerializationNullPointer
|
||||||
|
err = std_json.Unmarshal([]byte(`{"opt_null":"2006-01-02T15:04:05Z"}`), &v3)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, v3.OptNull.Has())
|
||||||
|
assert.NotNil(t, v3.OptNull.Value())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOptionalFromJson(t *testing.T) {
|
func TestOptionalFromJson(t *testing.T) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.dev/modules/charset"
|
||||||
"gitea.dev/modules/json"
|
"gitea.dev/modules/json"
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
"gitea.dev/modules/validation"
|
"gitea.dev/modules/validation"
|
||||||
@@ -261,6 +262,7 @@ func ParsePackage(r ReadSeekAt, optVersion ...string) (*PackageInfo, error) {
|
|||||||
cj.Readme = "README.md"
|
cj.Readme = "README.md"
|
||||||
}
|
}
|
||||||
dataReadmeMd, _ := readPackageFile(pkgExt, r, cj.Readme, 10*1024)
|
dataReadmeMd, _ := readPackageFile(pkgExt, r, cj.Readme, 10*1024)
|
||||||
|
dataReadmeMd = charset.ToUTF8DropErrors(dataReadmeMd)
|
||||||
|
|
||||||
// FIXME: legacy problem, the "Readme" field is abused, it should always be the path to the readme file
|
// FIXME: legacy problem, the "Readme" field is abused, it should always be the path to the readme file
|
||||||
if len(dataReadmeMd) == 0 {
|
if len(dataReadmeMd) == 0 {
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
package composer
|
package composer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"io"
|
"io"
|
||||||
@@ -13,6 +11,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.dev/modules/json"
|
"gitea.dev/modules/json"
|
||||||
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
"github.com/dsnet/compress/bzip2"
|
"github.com/dsnet/compress/bzip2"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -76,37 +75,8 @@ func TestCommentsUnmarshal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParsePackage(t *testing.T) {
|
func TestParsePackage(t *testing.T) {
|
||||||
createArchive := func(files map[string]string) []byte {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
archive := zip.NewWriter(&buf)
|
|
||||||
for name, content := range files {
|
|
||||||
w, _ := archive.Create(name)
|
|
||||||
_, _ = w.Write([]byte(content))
|
|
||||||
}
|
|
||||||
_ = archive.Close()
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
createArchiveTar := func(comp func(io.Writer) io.WriteCloser, files map[string]string) []byte {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
w := comp(&buf)
|
|
||||||
archive := tar.NewWriter(w)
|
|
||||||
for name, content := range files {
|
|
||||||
hdr := &tar.Header{
|
|
||||||
Name: name,
|
|
||||||
Mode: 0o600,
|
|
||||||
Size: int64(len(content)),
|
|
||||||
}
|
|
||||||
_ = archive.WriteHeader(hdr)
|
|
||||||
_, _ = archive.Write([]byte(content))
|
|
||||||
}
|
|
||||||
_ = w.Close()
|
|
||||||
_ = archive.Close()
|
|
||||||
return buf.Bytes()
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("MissingComposerFile", func(t *testing.T) {
|
t.Run("MissingComposerFile", func(t *testing.T) {
|
||||||
data := createArchive(map[string]string{"dummy.txt": ""})
|
data := test.WriteZipArchive(map[string]string{"dummy.txt": ""}).Bytes()
|
||||||
|
|
||||||
cp, err := ParsePackage(bytes.NewReader(data))
|
cp, err := ParsePackage(bytes.NewReader(data))
|
||||||
assert.Nil(t, cp)
|
assert.Nil(t, cp)
|
||||||
@@ -114,7 +84,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("MissingComposerFileInRoot", func(t *testing.T) {
|
t.Run("MissingComposerFileInRoot", func(t *testing.T) {
|
||||||
data := createArchive(map[string]string{"sub/sub/composer.json": ""})
|
data := test.WriteZipArchive(map[string]string{"sub/sub/composer.json": ""}).Bytes()
|
||||||
|
|
||||||
cp, err := ParsePackage(bytes.NewReader(data))
|
cp, err := ParsePackage(bytes.NewReader(data))
|
||||||
assert.Nil(t, cp)
|
assert.Nil(t, cp)
|
||||||
@@ -122,7 +92,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("InvalidComposerFile", func(t *testing.T) {
|
t.Run("InvalidComposerFile", func(t *testing.T) {
|
||||||
data := createArchive(map[string]string{"composer.json": ""})
|
data := test.WriteZipArchive(map[string]string{"composer.json": ""}).Bytes()
|
||||||
|
|
||||||
cp, err := ParsePackage(bytes.NewReader(data))
|
cp, err := ParsePackage(bytes.NewReader(data))
|
||||||
assert.Nil(t, cp)
|
assert.Nil(t, cp)
|
||||||
@@ -130,7 +100,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("InvalidPackageName", func(t *testing.T) {
|
t.Run("InvalidPackageName", func(t *testing.T) {
|
||||||
data := createArchive(map[string]string{"composer.json": "{}"})
|
data := test.WriteZipArchive(map[string]string{"composer.json": "{}"}).Bytes()
|
||||||
|
|
||||||
cp, err := ParsePackage(bytes.NewReader(data))
|
cp, err := ParsePackage(bytes.NewReader(data))
|
||||||
assert.Nil(t, cp)
|
assert.Nil(t, cp)
|
||||||
@@ -138,7 +108,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("InvalidPackageVersion", func(t *testing.T) {
|
t.Run("InvalidPackageVersion", func(t *testing.T) {
|
||||||
data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "version": "1.a.3"}`})
|
data := test.WriteZipArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "version": "1.a.3"}`}).Bytes()
|
||||||
|
|
||||||
cp, err := ParsePackage(bytes.NewReader(data))
|
cp, err := ParsePackage(bytes.NewReader(data))
|
||||||
assert.Nil(t, cp)
|
assert.Nil(t, cp)
|
||||||
@@ -146,7 +116,7 @@ func TestParsePackage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("InvalidReadmePath", func(t *testing.T) {
|
t.Run("InvalidReadmePath", func(t *testing.T) {
|
||||||
data := createArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "readme": "sub/README.md"}`})
|
data := test.WriteZipArchive(map[string]string{"composer.json": `{"name": "gitea/composer-package", "readme": "sub/README.md"}`}).Bytes()
|
||||||
|
|
||||||
cp, err := ParsePackage(bytes.NewReader(data))
|
cp, err := ParsePackage(bytes.NewReader(data))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -177,22 +147,29 @@ func TestParsePackage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("ValidZip", func(t *testing.T) {
|
t.Run("ValidZip", func(t *testing.T) {
|
||||||
data := createArchive(map[string]string{"composer.json": buildComposerContent(""), "README.md": readme})
|
data := test.WriteZipArchive(map[string]string{"composer.json": buildComposerContent(""), "README.md": readme}).Bytes()
|
||||||
assertValidPackage(t, data, "", "gitea-composer-package.zip")
|
assertValidPackage(t, data, "", "gitea-composer-package.zip")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ValidTarBz2", func(t *testing.T) {
|
t.Run("ValidTarBz2", func(t *testing.T) {
|
||||||
data := createArchiveTar(func(w io.Writer) io.WriteCloser {
|
data := test.WriteTarCompression(func(w io.Writer) io.WriteCloser {
|
||||||
bz2Writer, _ := bzip2.NewWriter(w, nil)
|
bz2Writer, _ := bzip2.NewWriter(w, nil)
|
||||||
return bz2Writer
|
return bz2Writer
|
||||||
}, map[string]string{"composer.json": buildComposerContent("1.0"), "README.md": readme})
|
}, map[string]string{"composer.json": buildComposerContent("1.0"), "README.md": readme}).Bytes()
|
||||||
assertValidPackage(t, data, "1.0", "gitea-composer-package.1.0.tar.bz2")
|
assertValidPackage(t, data, "1.0", "gitea-composer-package.1.0.tar.bz2")
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("ValidTarGz", func(t *testing.T) {
|
t.Run("ValidTarGz", func(t *testing.T) {
|
||||||
data := createArchiveTar(func(w io.Writer) io.WriteCloser {
|
data := test.WriteTarCompression(func(w io.Writer) io.WriteCloser {
|
||||||
return gzip.NewWriter(w)
|
return gzip.NewWriter(w)
|
||||||
}, map[string]string{"composer.json": buildComposerContent(""), "README.md": readme})
|
}, map[string]string{"composer.json": buildComposerContent(""), "README.md": readme}).Bytes()
|
||||||
assertValidPackage(t, data, "", "gitea-composer-package.tar.gz")
|
assertValidPackage(t, data, "", "gitea-composer-package.tar.gz")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("ReadmeEncoding", func(t *testing.T) {
|
||||||
|
data := test.WriteZipArchive(map[string]string{"composer.json": buildComposerContent(""), "README.md": "🌞🌞"[:6]}).Bytes()
|
||||||
|
cp, err := ParsePackage(bytes.NewReader(data))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "🌞 ", cp.Metadata.Readme)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.dev/modules/packages"
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
"gitea.dev/modules/validation"
|
"gitea.dev/modules/validation"
|
||||||
|
|
||||||
@@ -69,6 +70,8 @@ type Dependency struct {
|
|||||||
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
|
Alias string `json:"alias,omitempty" yaml:"alias,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxArchiveSize = 256 * 1024 * 1024 // tar members are inflated while looking for Chart.yaml
|
||||||
|
|
||||||
// ParseChartArchive parses the metadata of a Helm archive
|
// ParseChartArchive parses the metadata of a Helm archive
|
||||||
func ParseChartArchive(r io.Reader) (*Metadata, error) {
|
func ParseChartArchive(r io.Reader) (*Metadata, error) {
|
||||||
gzr, err := gzip.NewReader(r)
|
gzr, err := gzip.NewReader(r)
|
||||||
@@ -77,7 +80,7 @@ func ParseChartArchive(r io.Reader) (*Metadata, error) {
|
|||||||
}
|
}
|
||||||
defer gzr.Close()
|
defer gzr.Close()
|
||||||
|
|
||||||
tr := tar.NewReader(gzr)
|
tr := tar.NewReader(packages.NewLimitedReader(gzr, maxArchiveSize))
|
||||||
for {
|
for {
|
||||||
hd, err := tr.Next()
|
hd, err := tr.Next()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package packages
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"gitea.dev/modules/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrContentTooLarge = util.NewInvalidArgumentErrorf("content exceeds the size limit")
|
||||||
|
|
||||||
|
type limitedReader struct {
|
||||||
|
io.LimitedReader
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLimitedReader returns a reader that fails with ErrContentTooLarge once it reads a byte past limit
|
||||||
|
func NewLimitedReader(r io.Reader, limit int64) io.Reader {
|
||||||
|
return &limitedReader{io.LimitedReader{R: r, N: limit + 1}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *limitedReader) Read(p []byte) (int, error) {
|
||||||
|
n, err := l.LimitedReader.Read(p)
|
||||||
|
if l.N == 0 {
|
||||||
|
return max(n-1, 0), ErrContentTooLarge
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"compress/gzip"
|
"compress/gzip"
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
@@ -23,6 +25,8 @@ var (
|
|||||||
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
|
ErrInvalidName = util.NewInvalidArgumentErrorf("package name is invalid")
|
||||||
// ErrInvalidVersion indicates an invalid version in the metadata.gz file
|
// ErrInvalidVersion indicates an invalid version in the metadata.gz file
|
||||||
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
|
ErrInvalidVersion = util.NewInvalidArgumentErrorf("package version is invalid")
|
||||||
|
// ErrInvalidMetadata indicates metadata that would corrupt the generated index
|
||||||
|
ErrInvalidMetadata = util.NewInvalidArgumentErrorf("package metadata is invalid")
|
||||||
)
|
)
|
||||||
|
|
||||||
var globalVars = sync.OnceValue(func() (ret struct {
|
var globalVars = sync.OnceValue(func() (ret struct {
|
||||||
@@ -220,6 +224,19 @@ func parseMetadataFile(r io.Reader) (*Package, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
values := []string{m.Platform}
|
||||||
|
reqs := slices.Concat(m.RequiredRubyVersion, m.RequiredRubygemsVersion)
|
||||||
|
for _, dep := range slices.Concat(m.RuntimeDependencies, m.DevelopmentDependencies) {
|
||||||
|
values = append(values, dep.Name)
|
||||||
|
reqs = append(reqs, dep.Version...)
|
||||||
|
}
|
||||||
|
for _, req := range reqs {
|
||||||
|
values = append(values, req.Restriction, req.Version)
|
||||||
|
}
|
||||||
|
if slices.ContainsFunc(values, func(s string) bool { return strings.ContainsAny(s, "\r\n") }) {
|
||||||
|
return nil, ErrInvalidMetadata // the compact index is line-based, a line break would forge entries
|
||||||
|
}
|
||||||
|
|
||||||
return &Package{
|
return &Package{
|
||||||
Name: spec.Name,
|
Name: spec.Name,
|
||||||
Version: spec.Version.Version,
|
Version: spec.Version.Version,
|
||||||
|
|||||||
@@ -43,6 +43,19 @@ version:
|
|||||||
assert.Nil(t, rp)
|
assert.Nil(t, rp)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("LineBreakInIndexedMetadata", func(t *testing.T) {
|
||||||
|
for _, field := range []string{
|
||||||
|
`platform: "ruby\nx"`,
|
||||||
|
`dependencies: [{name: "d\nx"}]`,
|
||||||
|
`dependencies: [{name: d, requirement: {requirements: [[">=", {version: "1\nx"}]]}}]`,
|
||||||
|
`required_ruby_version: {requirements: [[">=", {version: "1\nx"}]]}`,
|
||||||
|
} {
|
||||||
|
rp, err := parseMetadataFile(test.CompressGzip("name: g\nversion:\n version: 1\n" + field + "\n"))
|
||||||
|
assert.ErrorIs(t, err, ErrInvalidMetadata, field)
|
||||||
|
assert.Nil(t, rp)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseMetadataFile(t *testing.T) {
|
func TestParseMetadataFile(t *testing.T) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func CanUserForkRepo(ctx context.Context, user *user_model.User, repo *repo_mode
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
for _, org := range ownedOrgs {
|
for _, org := range ownedOrgs {
|
||||||
if repo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, repo.ID) {
|
if CanUserForkBetweenOwners(repo.OwnerID, org.ID) && !repo_model.HasForkedRepo(ctx, org.ID, repo.ID) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dev/models/db"
|
||||||
|
repo_model "gitea.dev/models/repo"
|
||||||
|
"gitea.dev/models/unittest"
|
||||||
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/setting"
|
"gitea.dev/modules/setting"
|
||||||
"gitea.dev/modules/test"
|
"gitea.dev/modules/test"
|
||||||
|
|
||||||
@@ -13,7 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCanUserForkBetweenOwners(t *testing.T) {
|
func TestCanUserForkBetweenOwners(t *testing.T) {
|
||||||
defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner)
|
defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner)()
|
||||||
|
|
||||||
setting.Repository.AllowForkIntoSameOwner = true
|
setting.Repository.AllowForkIntoSameOwner = true
|
||||||
assert.True(t, CanUserForkBetweenOwners(1, 1))
|
assert.True(t, CanUserForkBetweenOwners(1, 1))
|
||||||
@@ -23,3 +27,26 @@ func TestCanUserForkBetweenOwners(t *testing.T) {
|
|||||||
assert.False(t, CanUserForkBetweenOwners(1, 1))
|
assert.False(t, CanUserForkBetweenOwners(1, 1))
|
||||||
assert.True(t, CanUserForkBetweenOwners(1, 2))
|
assert.True(t, CanUserForkBetweenOwners(1, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCanUserForkRepoIntoOwningOrg(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
defer test.MockVariableValue(&setting.Repository.AllowForkIntoSameOwner)()
|
||||||
|
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||||
|
// user2's own fork leaves org3, the owner, as the only target
|
||||||
|
assert.NoError(t, db.Insert(t.Context(), &repo_model.Repository{
|
||||||
|
OwnerID: 2, Name: "repo3-fork", LowerName: "repo3-fork",
|
||||||
|
IsFork: true, ForkID: 3,
|
||||||
|
}))
|
||||||
|
|
||||||
|
setting.Repository.AllowForkIntoSameOwner = true
|
||||||
|
ok, err := CanUserForkRepo(t.Context(), user2, repo3)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
setting.Repository.AllowForkIntoSameOwner = false
|
||||||
|
ok, err = CanUserForkRepo(t.Context(), user2, repo3)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
@@ -67,8 +67,13 @@ func StoreMissingLfsObjectsInRepository(ctx context.Context, repo *repo_model.Re
|
|||||||
errChan <- lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan)
|
errChan <- lfs.SearchPointerBlobs(ctx, gitRepo, pointerChan)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// some servers require `ref`, send the default branch for objects from all refs like
|
||||||
|
// `git lfs fetch --all` does on the main branch: it uses current branch as `ref`:
|
||||||
|
// https://github.com/git-lfs/git-lfs/blob/58a763d09d5fe4b7ea85c89ec799d71c5b0dc1da/commands/commands.go#L124-L137
|
||||||
|
// `man git-lfs-fetch` said that "--all" can fetch all objects, so the default branch should work
|
||||||
|
ref := git.RefNameFromBranch(repo.DefaultBranch).String()
|
||||||
downloadObjects := func(pointers []lfs.Pointer) error {
|
downloadObjects := func(pointers []lfs.Pointer) error {
|
||||||
err := lfsClient.Download(ctx, pointers, func(p lfs.Pointer, content io.ReadCloser, objectError error) error {
|
err := lfsClient.Download(ctx, ref, pointers, func(p lfs.Pointer, content io.ReadCloser, objectError error) error {
|
||||||
if errors.Is(objectError, lfs.ErrObjectNotExist) {
|
if errors.Is(objectError, lfs.ErrObjectNotExist) {
|
||||||
log.Warn("Ignoring missing upstream LFS object %-v: %v", p, objectError)
|
log.Warn("Ignoring missing upstream LFS object %-v: %v", p, objectError)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ func loadGitFrom(rootCfg ConfigProvider) {
|
|||||||
GitConfig.Options = make(map[string]string)
|
GitConfig.Options = make(map[string]string)
|
||||||
GitConfig.SetOption("diff.algorithm", "histogram")
|
GitConfig.SetOption("diff.algorithm", "histogram")
|
||||||
GitConfig.SetOption("core.logAllRefUpdates", "true")
|
GitConfig.SetOption("core.logAllRefUpdates", "true")
|
||||||
|
GitConfig.SetOption("maintenance.strategy", "gc") // git 2.54's push-time default "geometric" delays pushes and repacks concurrently with other writes
|
||||||
|
|
||||||
secGitReflog := rootCfg.Section("git.reflog")
|
secGitReflog := rootCfg.Section("git.reflog")
|
||||||
if secGitReflog.HasKey("ENABLED") {
|
if secGitReflog.HasKey("ENABLED") {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ a.b = 1
|
|||||||
loadGitFrom(cfg)
|
loadGitFrom(cfg)
|
||||||
assert.Equal(t, "1", GitConfig.Options["a.b"])
|
assert.Equal(t, "1", GitConfig.Options["a.b"])
|
||||||
assert.Equal(t, "histogram", GitConfig.Options["diff.algorithm"])
|
assert.Equal(t, "histogram", GitConfig.Options["diff.algorithm"])
|
||||||
|
assert.Equal(t, "gc", GitConfig.Options["maintenance.strategy"])
|
||||||
|
|
||||||
cfg, err = NewConfigProviderFromData(`
|
cfg, err = NewConfigProviderFromData(`
|
||||||
[git.config]
|
[git.config]
|
||||||
|
|||||||
@@ -56,7 +56,9 @@ type EditLabelOption struct {
|
|||||||
// IssueLabelsOption a collection of labels
|
// IssueLabelsOption a collection of labels
|
||||||
type IssueLabelsOption struct {
|
type IssueLabelsOption struct {
|
||||||
// Labels can be a list of integers representing label IDs
|
// Labels can be a list of integers representing label IDs
|
||||||
// or a list of strings representing label names
|
// or a list of strings representing label names.
|
||||||
|
// If a repository label and an organization label share a name,
|
||||||
|
// the repository label is used.
|
||||||
Labels []any `json:"labels"`
|
Labels []any `json:"labels"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ package structs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.dev/modules/optional"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Milestone milestone is a collection of issues on one repository
|
// Milestone milestone is a collection of issues on one repository
|
||||||
@@ -54,6 +56,7 @@ type EditMilestoneOption struct {
|
|||||||
// State indicates the updated state of the milestone
|
// State indicates the updated state of the milestone
|
||||||
// enum: ["open","closed"]
|
// enum: ["open","closed"]
|
||||||
State *string `json:"state"`
|
State *string `json:"state"`
|
||||||
// Deadline is the updated due date for the milestone
|
// Deadline is the due date for the milestone, use null or empty string to remove the deadline
|
||||||
Deadline *time.Time `json:"due_on"`
|
// swagger:strfmt date-time
|
||||||
|
Deadline optional.Option[*string] `json:"due_on,omitzero"`
|
||||||
}
|
}
|
||||||
@@ -158,6 +158,23 @@ com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
|
|||||||
assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject("see https://example.com/x here", "https://example.com/link", mockRepo))
|
assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject("see https://example.com/x here", "https://example.com/link", mockRepo))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("RenderCommitMessageLinkSubjectEmoji", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.UI.CustomEmojisMap, map[string]string{"custom-emoji": ":custom-emoji:"})()
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name, subject, content string
|
||||||
|
}{
|
||||||
|
{"native", "Bug ❌ Feature ✅", `Bug <span class="emoji" data-alias="x">❌</span> Feature <span class="emoji" data-alias="white_check_mark">✅</span>`},
|
||||||
|
{"shortcode", "Ship :+1: today", `Ship <span class="emoji" data-alias="+1">👍</span> today`},
|
||||||
|
{"emoji only", "👍", `<span class="emoji" data-alias="+1">👍</span>`},
|
||||||
|
{"custom", "Ship :custom-emoji:", `Ship <span class="emoji"><img alt=":custom-emoji:" src="/assets/img/emoji/custom-emoji.png"/></span>`},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
expected := `<span class="title-full-link-hover"><a href="https://example.com/link" class="muted title-full-link">` + tt.content + `</a></span>`
|
||||||
|
assert.EqualValues(t, expected, newTestRenderUtils(t).RenderCommitMessageLinkSubject(tt.subject, "https://example.com/link", mockRepo))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("RenderIssueTitle", func(t *testing.T) {
|
t.Run("RenderIssueTitle", func(t *testing.T) {
|
||||||
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
defer test.MockVariableValue(&markup.RenderBehaviorForTesting.DisableAdditionalAttributes, true)()
|
||||||
expected := ` space @mention-user<SPACE><SPACE>
|
expected := ` space @mention-user<SPACE><SPACE>
|
||||||
|
|||||||
@@ -3556,7 +3556,7 @@
|
|||||||
"units.error.no_unit_allowed_repo": "You are not allowed to access any section of this repository.",
|
"units.error.no_unit_allowed_repo": "You are not allowed to access any section of this repository.",
|
||||||
"units.error.unit_not_allowed": "You are not allowed to access this repository section.",
|
"units.error.unit_not_allowed": "You are not allowed to access this repository section.",
|
||||||
"packages.title": "Packages",
|
"packages.title": "Packages",
|
||||||
"packages.desc": "Manage repository packages.",
|
"packages.desc": "Access all packages of the organization, regardless of the team's repositories.",
|
||||||
"packages.empty": "There are no packages yet.",
|
"packages.empty": "There are no packages yet.",
|
||||||
"packages.no_metadata": "No metadata.",
|
"packages.no_metadata": "No metadata.",
|
||||||
"packages.empty.documentation": "For more information on the package registry, see <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the documentation</a>.",
|
"packages.empty.documentation": "For more information on the package registry, see <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"%s\">the documentation</a>.",
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export default defineConfig({
|
|||||||
name: 'firefox',
|
name: 'firefox',
|
||||||
use: {
|
use: {
|
||||||
...devices['Desktop Firefox'],
|
...devices['Desktop Firefox'],
|
||||||
|
launchOptions: {
|
||||||
|
firefoxUserPrefs: {
|
||||||
|
'dom.events.testing.asyncClipboard': true, // disable paste prompt
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -281,7 +281,7 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
|
|||||||
artifact.FileCompressedSize != chunksTotalSize {
|
artifact.FileCompressedSize != chunksTotalSize {
|
||||||
artifact.FileSize = fileRealTotalSize
|
artifact.FileSize = fileRealTotalSize
|
||||||
artifact.FileCompressedSize = chunksTotalSize
|
artifact.FileCompressedSize = chunksTotalSize
|
||||||
artifact.ContentEncodingOrType = ctx.Req.Header.Get("Content-Encoding")
|
artifact.ContentEncodingOrType = util.Iif(ctx.Req.Header.Get("Content-Encoding") == actions.ContentEncodingV3Gzip, actions.ContentEncodingV3Gzip, "") // other values could be served as a v4 Content-Type
|
||||||
if err := actions.UpdateArtifact(ctx, artifact, "file_size", "file_compressed_size", "content_encoding"); err != nil {
|
if err := actions.UpdateArtifact(ctx, artifact, "file_size", "file_compressed_size", "content_encoding"); err != nil {
|
||||||
log.Error("Error update artifact: %v", err)
|
log.Error("Error update artifact: %v", err)
|
||||||
ctx.HTTPError(http.StatusInternalServerError, "Error update artifact")
|
ctx.HTTPError(http.StatusInternalServerError, "Error update artifact")
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package runner
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -17,6 +18,7 @@ import (
|
|||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
|
|
||||||
"connectrpc.com/connect"
|
"connectrpc.com/connect"
|
||||||
|
lru "github.com/hashicorp/golang-lru/v2"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"google.golang.org/grpc/status"
|
||||||
)
|
)
|
||||||
@@ -26,6 +28,10 @@ const (
|
|||||||
tokenHeaderKey = protocol.TokenHeader
|
tokenHeaderKey = protocol.TokenHeader
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type runnerTokenHash struct{ salt, hash string }
|
||||||
|
|
||||||
|
var verifiedRunnerTokens, _ = lru.New[runnerTokenHash, [sha256.Size]byte](10000) // not auth_model.TokenCache, whose small default size thrashes once more runners poll in turn
|
||||||
|
|
||||||
var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unaryFunc connect.UnaryFunc) connect.UnaryFunc {
|
var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unaryFunc connect.UnaryFunc) connect.UnaryFunc {
|
||||||
return func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) {
|
return func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) {
|
||||||
methodName := getMethodName(request)
|
methodName := getMethodName(request)
|
||||||
@@ -42,7 +48,7 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar
|
|||||||
}
|
}
|
||||||
return nil, status.Error(codes.Internal, err.Error())
|
return nil, status.Error(codes.Internal, err.Error())
|
||||||
}
|
}
|
||||||
if !util.CryptoConstTimeEqual(runner.TokenHash, auth_model.HashToken(token, runner.TokenSalt)) {
|
if !verifyRunnerToken(runner, token) {
|
||||||
return nil, status.Error(codes.Unauthenticated, "unregistered runner")
|
return nil, status.Error(codes.Unauthenticated, "unregistered runner")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +79,18 @@ var withRunner = connect.WithInterceptors(connect.UnaryInterceptorFunc(func(unar
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
func verifyRunnerToken(runner *actions_model.ActionRunner, token string) bool {
|
||||||
|
key, digest := runnerTokenHash{runner.TokenSalt, runner.TokenHash}, sha256.Sum256([]byte(token))
|
||||||
|
if cached, ok := verifiedRunnerTokens.Get(key); ok && util.CryptoConstTimeEqual(cached[:], digest[:]) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if !util.CryptoConstTimeEqual(runner.TokenHash, auth_model.HashToken(token, runner.TokenSalt)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
verifiedRunnerTokens.Add(key, digest)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func getMethodName(req connect.AnyRequest) string {
|
func getMethodName(req connect.AnyRequest) string {
|
||||||
splits := strings.Split(req.Spec().Procedure, "/")
|
splits := strings.Split(req.Spec().Procedure, "/")
|
||||||
if len(splits) > 0 {
|
if len(splits) > 0 {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package runner
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
actions_model "gitea.dev/models/actions"
|
||||||
|
auth_model "gitea.dev/models/auth"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVerifyRunnerToken(t *testing.T) {
|
||||||
|
verifiedRunnerTokens.Purge()
|
||||||
|
runner := &actions_model.ActionRunner{TokenSalt: "salt", TokenHash: auth_model.HashToken("token", "salt")}
|
||||||
|
assert.True(t, verifyRunnerToken(runner, "token"))
|
||||||
|
assert.True(t, verifyRunnerToken(runner, "token"))
|
||||||
|
assert.False(t, verifyRunnerToken(runner, "wrong"))
|
||||||
|
|
||||||
|
other := &actions_model.ActionRunner{TokenSalt: "salt", TokenHash: auth_model.HashToken("other", "salt")}
|
||||||
|
assert.False(t, verifyRunnerToken(other, "token"))
|
||||||
|
assert.Equal(t, [][sha256.Size]byte{sha256.Sum256([]byte("token"))}, verifiedRunnerTokens.Values())
|
||||||
|
}
|
||||||
@@ -244,7 +244,7 @@ func PostBlobsUploads(ctx *context.Context) {
|
|||||||
Digest: mount,
|
Digest: mount,
|
||||||
})
|
})
|
||||||
if blob != nil {
|
if blob != nil {
|
||||||
accessible, err := packages_model.IsBlobAccessibleForUser(ctx, blob.Blob.ID, ctx.Doer)
|
accessible, err := packages_model.IsBlobAccessibleForUser(ctx, blob.Blob.ID, ctx.Doer, context.TokenIsPublicOnly(ctx))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
apiError(ctx, http.StatusInternalServerError, err)
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -481,7 +481,7 @@ func SearchUsers(ctx *context.APIContext) {
|
|||||||
|
|
||||||
searchOpts := user_model.SearchUserOptions{
|
searchOpts := user_model.SearchUserOptions{
|
||||||
Actor: ctx.Doer,
|
Actor: ctx.Doer,
|
||||||
Types: []user_model.UserType{user_model.UserTypeIndividual},
|
Types: user_model.AdminListedUserTypes,
|
||||||
LoginName: ctx.FormTrim("login_name"),
|
LoginName: ctx.FormTrim("login_name"),
|
||||||
SourceID: ctx.FormInt64("source_id"),
|
SourceID: ctx.FormInt64("source_id"),
|
||||||
Keyword: ctx.FormTrim("q"),
|
Keyword: ctx.FormTrim("q"),
|
||||||
|
|||||||
+46
-30
@@ -317,8 +317,14 @@ func rejectPublicOnly() func(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func contextAuthenticatedUser() func(ctx *context.APIContext) {
|
func authenticatedUserAsContextUser(allowSystemUser bool) func(ctx *context.APIContext) {
|
||||||
return func(ctx *context.APIContext) {
|
return func(ctx *context.APIContext) {
|
||||||
|
isIndividualOrBot := ctx.Doer.IsIndividual() || ctx.Doer.IsTypeBot()
|
||||||
|
isSystem := ctx.Doer.ID < 0
|
||||||
|
if !isIndividualOrBot || isSystem && !allowSystemUser {
|
||||||
|
ctx.APIError(http.StatusForbidden, "this endpoint is only available for individual user or bot accounts")
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.ContextUser = ctx.Doer
|
ctx.ContextUser = ctx.Doer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -492,6 +498,9 @@ func reqAnyRepoReader() func(ctx *context.APIContext) {
|
|||||||
// reqOrgOwnership user should be an organization owner, or a site admin
|
// reqOrgOwnership user should be an organization owner, or a site admin
|
||||||
func reqOrgOwnership() func(ctx *context.APIContext) {
|
func reqOrgOwnership() func(ctx *context.APIContext) {
|
||||||
return func(ctx *context.APIContext) {
|
return func(ctx *context.APIContext) {
|
||||||
|
if rejectPublicOnly()(ctx); ctx.Written() { // org ownership reaches the org's private repos (hooks, teams, secrets)
|
||||||
|
return
|
||||||
|
}
|
||||||
if ctx.IsUserSiteAdmin() {
|
if ctx.IsUserSiteAdmin() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -876,6 +885,10 @@ func mustEnableRepoProjects(ctx *context.APIContext) {
|
|||||||
|
|
||||||
// FIXME: for consistency, maybe most mustNotBeArchived checks should be replaced with mustEnableEditor
|
// FIXME: for consistency, maybe most mustNotBeArchived checks should be replaced with mustEnableEditor
|
||||||
func mustNotBeArchived(ctx *context.APIContext) {
|
func mustNotBeArchived(ctx *context.APIContext) {
|
||||||
|
switch ctx.Req.Method {
|
||||||
|
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
||||||
|
return
|
||||||
|
}
|
||||||
if ctx.Repo.Repository.IsArchived {
|
if ctx.Repo.Repository.IsArchived {
|
||||||
ctx.APIError(http.StatusLocked, "repo is archived")
|
ctx.APIError(http.StatusLocked, "repo is archived")
|
||||||
return
|
return
|
||||||
@@ -1155,8 +1168,11 @@ func Routes() *web.Router {
|
|||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
||||||
|
|
||||||
// Users (requires user scope)
|
// Users (requires user scope)
|
||||||
|
contextRealDoerUser := authenticatedUserAsContextUser(false)
|
||||||
|
contextRealOrSystemUser := authenticatedUserAsContextUser(true)
|
||||||
|
m.Get("/user", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextRealOrSystemUser, checkTokenPublicOnly(), user.GetAuthenticatedUser)
|
||||||
m.Group("/user", func() {
|
m.Group("/user", func() {
|
||||||
m.Get("", user.GetAuthenticatedUser)
|
m.Get("/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
|
||||||
m.Group("/settings", func() {
|
m.Group("/settings", func() {
|
||||||
m.Get("", user.GetUserSettings)
|
m.Get("", user.GetUserSettings)
|
||||||
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
|
m.Patch("", bind(api.UserSettingsOptions{}), user.UpdateUserSettings)
|
||||||
@@ -1276,13 +1292,14 @@ func Routes() *web.Router {
|
|||||||
m.Delete("", user.UnblockUser)
|
m.Delete("", user.UnblockUser)
|
||||||
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
|
}, context.UserAssignmentAPI(), checkTokenPublicOnly())
|
||||||
}, rejectPublicOnly())
|
}, rejectPublicOnly())
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextAuthenticatedUser(), checkTokenPublicOnly())
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken(), contextRealDoerUser, checkTokenPublicOnly())
|
||||||
|
|
||||||
// Repositories (requires repo scope, org scope)
|
// Repositories (requires repo scope, org scope)
|
||||||
m.Post("/org/{org}/repos",
|
m.Post("/org/{org}/repos",
|
||||||
// FIXME: we need org in context
|
// FIXME: we need org in context
|
||||||
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository),
|
tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository),
|
||||||
reqToken(),
|
reqToken(),
|
||||||
|
rejectPublicOnly(),
|
||||||
bind(api.CreateRepoOption{}),
|
bind(api.CreateRepoOption{}),
|
||||||
repo.CreateOrgRepoDeprecated)
|
repo.CreateOrgRepoDeprecated)
|
||||||
|
|
||||||
@@ -1303,7 +1320,7 @@ func Routes() *web.Router {
|
|||||||
m.Combo("").Get(reqAnyRepoReader(), repo.Get).
|
m.Combo("").Get(reqAnyRepoReader(), repo.Get).
|
||||||
Delete(reqToken(), reqRepoDangerZone(), repo.Delete).
|
Delete(reqToken(), reqRepoDangerZone(), repo.Delete).
|
||||||
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
|
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
|
||||||
m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
|
m.Post("/generate", reqToken(), rejectPublicOnly(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
|
||||||
m.Group("/transfer", func() {
|
m.Group("/transfer", func() {
|
||||||
m.Post("", reqRepoDangerZone(), bind(api.TransferRepoOption{}), repo.Transfer)
|
m.Post("", reqRepoDangerZone(), bind(api.TransferRepoOption{}), repo.Transfer)
|
||||||
m.Post("/accept", repo.AcceptTransfer)
|
m.Post("/accept", repo.AcceptTransfer)
|
||||||
@@ -1483,7 +1500,7 @@ func Routes() *web.Router {
|
|||||||
Get(repo.GetReleaseByTag).
|
Get(repo.GetReleaseByTag).
|
||||||
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
|
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
|
||||||
})
|
})
|
||||||
}, reqRepoReader(unit.TypeReleases))
|
}, reqRepoReader(unit.TypeReleases), mustNotBeArchived)
|
||||||
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.MirrorSync)
|
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.MirrorSync)
|
||||||
m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), mustNotBeArchived, repo.PushMirrorSync)
|
m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), mustNotBeArchived, repo.PushMirrorSync)
|
||||||
m.Group("/push_mirrors", func() {
|
m.Group("/push_mirrors", func() {
|
||||||
@@ -1630,14 +1647,14 @@ func Routes() *web.Router {
|
|||||||
m.Group("/{username}/{reponame}", func() {
|
m.Group("/{username}/{reponame}", func() {
|
||||||
m.Group("/issues", func() {
|
m.Group("/issues", func() {
|
||||||
m.Combo("").Get(repo.ListIssues).
|
m.Combo("").Get(repo.ListIssues).
|
||||||
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue)
|
Post(reqToken(), bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue)
|
||||||
m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues)
|
m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues)
|
||||||
m.Group("/comments", func() {
|
m.Group("/comments", func() {
|
||||||
m.Get("", repo.ListRepoIssueComments)
|
m.Get("", repo.ListRepoIssueComments)
|
||||||
m.Group("/{id}", func() {
|
m.Group("/{id}", func() {
|
||||||
m.Combo("").
|
m.Combo("").
|
||||||
Get(repo.GetIssueComment).
|
Get(repo.GetIssueComment).
|
||||||
Patch(mustNotBeArchived, reqToken(), bind(api.EditIssueCommentOption{}), repo.EditIssueComment).
|
Patch(reqToken(), bind(api.EditIssueCommentOption{}), repo.EditIssueComment).
|
||||||
Delete(reqToken(), repo.DeleteIssueComment)
|
Delete(reqToken(), repo.DeleteIssueComment)
|
||||||
m.Combo("/reactions").
|
m.Combo("/reactions").
|
||||||
Get(repo.GetIssueCommentReactions).
|
Get(repo.GetIssueCommentReactions).
|
||||||
@@ -1646,35 +1663,35 @@ func Routes() *web.Router {
|
|||||||
m.Group("/assets", func() {
|
m.Group("/assets", func() {
|
||||||
m.Combo("").
|
m.Combo("").
|
||||||
Get(repo.ListIssueCommentAttachments).
|
Get(repo.ListIssueCommentAttachments).
|
||||||
Post(reqToken(), mustNotBeArchived, repo.CreateIssueCommentAttachment)
|
Post(reqToken(), repo.CreateIssueCommentAttachment)
|
||||||
m.Combo("/{attachment_id}").
|
m.Combo("/{attachment_id}").
|
||||||
Get(repo.GetIssueCommentAttachment).
|
Get(repo.GetIssueCommentAttachment).
|
||||||
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
|
Patch(reqToken(), bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
|
||||||
Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueCommentAttachment)
|
Delete(reqToken(), repo.DeleteIssueCommentAttachment)
|
||||||
}, mustEnableAttachments)
|
}, mustEnableAttachments)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
m.Group("/{index}", func() {
|
m.Group("/{index}", func() {
|
||||||
m.Combo("").Get(repo.GetIssue).
|
m.Combo("").Get(repo.GetIssue).
|
||||||
Patch(reqToken(), mustNotBeArchived, bind(api.EditIssueOption{}), repo.EditIssue).
|
Patch(reqToken(), bind(api.EditIssueOption{}), repo.EditIssue).
|
||||||
Delete(reqToken(), reqAdmin(), mustNotBeArchived, context.ReferencesGitRepo(), repo.DeleteIssue)
|
Delete(reqToken(), reqAdmin(), context.ReferencesGitRepo(), repo.DeleteIssue)
|
||||||
m.Combo("/assignees").
|
m.Combo("/assignees").
|
||||||
Post(reqToken(), mustNotBeArchived, bind(api.IssueAssigneesOption{}), repo.AddIssueAssignees).
|
Post(reqToken(), bind(api.IssueAssigneesOption{}), repo.AddIssueAssignees).
|
||||||
Delete(reqToken(), mustNotBeArchived, bind(api.IssueAssigneesOption{}), repo.DeleteIssueAssignees)
|
Delete(reqToken(), bind(api.IssueAssigneesOption{}), repo.DeleteIssueAssignees)
|
||||||
m.Get("/assignees/{assignee}", repo.CheckIssueAssignee)
|
m.Get("/assignees/{assignee}", repo.CheckIssueAssignee)
|
||||||
m.Group("/comments", func() {
|
m.Group("/comments", func() {
|
||||||
m.Combo("").Get(repo.ListIssueComments).
|
m.Combo("").Get(repo.ListIssueComments).
|
||||||
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
|
Post(reqToken(), bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment)
|
||||||
m.Combo("/{id}", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated).
|
m.Combo("/{id}", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated).
|
||||||
Delete(repo.DeleteIssueCommentDeprecated)
|
Delete(repo.DeleteIssueCommentDeprecated)
|
||||||
})
|
})
|
||||||
m.Get("/timeline", repo.ListIssueCommentsAndTimeline)
|
m.Get("/timeline", repo.ListIssueCommentsAndTimeline)
|
||||||
m.Group("/labels", func() {
|
m.Group("/labels", func() {
|
||||||
m.Combo("").Get(repo.ListIssueLabels).
|
m.Combo("").Get(repo.ListIssueLabels).
|
||||||
Post(reqToken(), mustNotBeArchived, bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
|
Post(reqToken(), bind(api.IssueLabelsOption{}), repo.AddIssueLabels).
|
||||||
Put(reqToken(), mustNotBeArchived, bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
|
Put(reqToken(), bind(api.IssueLabelsOption{}), repo.ReplaceIssueLabels).
|
||||||
Delete(reqToken(), mustNotBeArchived, repo.ClearIssueLabels)
|
Delete(reqToken(), repo.ClearIssueLabels)
|
||||||
m.Delete("/{id}", reqToken(), mustNotBeArchived, repo.DeleteIssueLabel)
|
m.Delete("/{id}", reqToken(), repo.DeleteIssueLabel)
|
||||||
})
|
})
|
||||||
m.Group("/times", func() {
|
m.Group("/times", func() {
|
||||||
m.Combo("").
|
m.Combo("").
|
||||||
@@ -1702,16 +1719,16 @@ func Routes() *web.Router {
|
|||||||
m.Group("/assets", func() {
|
m.Group("/assets", func() {
|
||||||
m.Combo("").
|
m.Combo("").
|
||||||
Get(repo.ListIssueAttachments).
|
Get(repo.ListIssueAttachments).
|
||||||
Post(reqToken(), mustNotBeArchived, repo.CreateIssueAttachment)
|
Post(reqToken(), repo.CreateIssueAttachment)
|
||||||
m.Combo("/{attachment_id}").
|
m.Combo("/{attachment_id}").
|
||||||
Get(repo.GetIssueAttachment).
|
Get(repo.GetIssueAttachment).
|
||||||
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
|
Patch(reqToken(), bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
|
||||||
Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueAttachment)
|
Delete(reqToken(), repo.DeleteIssueAttachment)
|
||||||
}, mustEnableAttachments)
|
}, mustEnableAttachments)
|
||||||
m.Combo("/dependencies").
|
m.Combo("/dependencies").
|
||||||
Get(repo.GetIssueDependencies).
|
Get(repo.GetIssueDependencies).
|
||||||
Post(reqToken(), mustNotBeArchived, bind(api.IssueMeta{}), repo.CreateIssueDependency).
|
Post(reqToken(), bind(api.IssueMeta{}), repo.CreateIssueDependency).
|
||||||
Delete(reqToken(), mustNotBeArchived, bind(api.IssueMeta{}), repo.RemoveIssueDependency)
|
Delete(reqToken(), bind(api.IssueMeta{}), repo.RemoveIssueDependency)
|
||||||
m.Combo("/blocks").
|
m.Combo("/blocks").
|
||||||
Get(repo.GetIssueBlocks).
|
Get(repo.GetIssueBlocks).
|
||||||
Post(reqToken(), bind(api.IssueMeta{}), repo.CreateIssueBlocking).
|
Post(reqToken(), bind(api.IssueMeta{}), repo.CreateIssueBlocking).
|
||||||
@@ -1744,9 +1761,9 @@ func Routes() *web.Router {
|
|||||||
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
|
Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone)
|
||||||
})
|
})
|
||||||
m.Group("/projects", func() {
|
m.Group("/projects", func() {
|
||||||
addProjectRoutes(m, reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived)
|
addProjectRoutes(m, reqToken(), reqRepoWriter(unit.TypeProjects))
|
||||||
}, reqRepoReader(unit.TypeProjects), mustEnableRepoProjects)
|
}, reqRepoReader(unit.TypeProjects), mustEnableRepoProjects)
|
||||||
}, repoAssignment(), checkTokenPublicOnly())
|
}, repoAssignment(), checkTokenPublicOnly(), mustNotBeArchived)
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue))
|
||||||
|
|
||||||
// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
|
// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
|
||||||
@@ -1772,12 +1789,11 @@ func Routes() *web.Router {
|
|||||||
}, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
|
}, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())
|
||||||
|
|
||||||
// Organizations
|
// Organizations
|
||||||
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), checkTokenPublicOnly(), org.ListMyOrgs)
|
|
||||||
m.Group("/users/{username}/orgs", func() {
|
m.Group("/users/{username}/orgs", func() {
|
||||||
m.Get("", reqToken(), org.ListUserOrgs)
|
m.Get("", reqToken(), org.ListUserOrgs)
|
||||||
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
|
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
|
||||||
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
|
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), rejectPublicOnly(), bind(api.CreateOrgOption{}), org.Create)
|
||||||
m.Get("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), org.GetAll)
|
m.Get("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), org.GetAll)
|
||||||
m.Group("/orgs/{org}", func() {
|
m.Group("/orgs/{org}", func() {
|
||||||
m.Combo("").Get(org.Get).
|
m.Combo("").Get(org.Get).
|
||||||
@@ -1785,7 +1801,7 @@ func Routes() *web.Router {
|
|||||||
Delete(reqToken(), reqOrgOwnership(), org.Delete)
|
Delete(reqToken(), reqOrgOwnership(), org.Delete)
|
||||||
m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename)
|
m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename)
|
||||||
m.Combo("/repos").Get(user.ListOrgRepos).
|
m.Combo("/repos").Get(user.ListOrgRepos).
|
||||||
Post(reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), bind(api.CreateRepoOption{}), repo.CreateOrgRepo).
|
Post(reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), rejectPublicOnly(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo).
|
||||||
Delete(reqToken(), reqOrgOwnership(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), org.DeleteOrgRepos)
|
Delete(reqToken(), reqOrgOwnership(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), org.DeleteOrgRepos)
|
||||||
m.Group("/members", func() {
|
m.Group("/members", func() {
|
||||||
m.Get("", reqToken(), org.ListMembers)
|
m.Get("", reqToken(), org.ListMembers)
|
||||||
@@ -1921,7 +1937,7 @@ func Routes() *web.Router {
|
|||||||
m.Get("/runs", admin.ListWorkflowRuns)
|
m.Get("/runs", admin.ListWorkflowRuns)
|
||||||
m.Get("/jobs", admin.ListWorkflowJobs)
|
m.Get("/jobs", admin.ListWorkflowJobs)
|
||||||
})
|
})
|
||||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin(), rejectPublicOnly())
|
||||||
|
|
||||||
m.Group("/topics", func() {
|
m.Group("/topics", func() {
|
||||||
m.Get("/search", repo.TopicSearch)
|
m.Get("/search", repo.TopicSearch)
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ func GetEditorconfig(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
ec, _, err := ctx.Repo.GetEditorconfig(ctx, ctx.Repo.Commit)
|
ec, _, err := ctx.Repo.GetEditorconfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorAuto(err)
|
ctx.APIErrorAuto(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"gitea.dev/models/perm"
|
"gitea.dev/models/perm"
|
||||||
access_model "gitea.dev/models/perm/access"
|
access_model "gitea.dev/models/perm/access"
|
||||||
repo_model "gitea.dev/models/repo"
|
repo_model "gitea.dev/models/repo"
|
||||||
user_model "gitea.dev/models/user"
|
|
||||||
"gitea.dev/modules/optional"
|
"gitea.dev/modules/optional"
|
||||||
api "gitea.dev/modules/structs"
|
api "gitea.dev/modules/structs"
|
||||||
"gitea.dev/modules/util"
|
"gitea.dev/modules/util"
|
||||||
@@ -166,13 +165,7 @@ func CreateFork(ctx *context.APIContext) {
|
|||||||
Description: repo.Description,
|
Description: repo.Description,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
|
ctx.APIErrorAuto(err)
|
||||||
ctx.APIError(http.StatusConflict, err.Error())
|
|
||||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
|
||||||
ctx.APIError(http.StatusForbidden, err.Error())
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -337,8 +337,8 @@ func ListIssues(ctx *context.APIContext) {
|
|||||||
keyword := ctx.FormTrim("q")
|
keyword := ctx.FormTrim("q")
|
||||||
|
|
||||||
var labelIDs []int64
|
var labelIDs []int64
|
||||||
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
|
if labelsParam := ctx.FormTrim("labels"); labelsParam != "" {
|
||||||
labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted)
|
labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, strings.Split(labelsParam, ","))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
@@ -513,17 +513,12 @@ func GetIssue(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
if err := issue.LoadAttributes(ctx); err != nil {
|
||||||
ctx.APIErrorNotFound()
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue))
|
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue))
|
||||||
@@ -698,19 +693,13 @@ func EditIssue(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
form := web.GetForm[*api.EditIssueOption](ctx)
|
form := web.GetForm[*api.EditIssueOption](ctx)
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
issue.Repo = ctx.Repo.Repository
|
|
||||||
canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
|
canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
|
||||||
|
|
||||||
err = issue.LoadAttributes(ctx)
|
err := issue.LoadAttributes(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
@@ -901,13 +890,12 @@ func DeleteIssue(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
// "423":
|
// "423":
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorAuto(err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = issue_service.DeleteIssue(ctx, ctx.Doer, issue); err != nil {
|
if err := issue_service.DeleteIssue(ctx, ctx.Doer, issue); err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -953,13 +941,8 @@ func UpdateIssueDeadline(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
form := web.GetForm[*api.EditDeadlineOption](ctx)
|
form := web.GetForm[*api.EditDeadlineOption](ctx)
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -999,3 +982,17 @@ func closeOrReopenIssue(ctx *context.APIContext, issue *issues_model.Issue, stat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRepoIssueByIndexForDoer(ctx *context.APIContext, index int64) *issues_model.Issue {
|
||||||
|
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, index)
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorAuto(err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
||||||
|
ctx.APIErrorNotFound()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
issue.Repo = ctx.Repo.Repository
|
||||||
|
return issue
|
||||||
|
}
|
||||||
@@ -146,14 +146,8 @@ func CheckIssueAssignee(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorAuto(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,9 +181,8 @@ func checkAssignableUser(ctx *context.APIContext, assigneeName string, repo *rep
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateIssueAssignees(ctx *context.APIContext, opts api.IssueAssigneesOption, isAdd bool) {
|
func updateIssueAssignees(ctx *context.APIContext, opts api.IssueAssigneesOption, isAdd bool) {
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorAuto(err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func GetIssueAttachment(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
|
|
||||||
issue := getIssueFromContext(ctx)
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if issue == nil {
|
if issue == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ func ListIssueAttachments(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
|
|
||||||
issue := getIssueFromContext(ctx)
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if issue == nil {
|
if issue == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ func CreateIssueAttachment(ctx *context.APIContext) {
|
|||||||
// "423":
|
// "423":
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
issue := getIssueFromContext(ctx)
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if issue == nil {
|
if issue == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -333,20 +333,8 @@ func DeleteIssueAttachment(ctx *context.APIContext) {
|
|||||||
ctx.Status(http.StatusNoContent)
|
ctx.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIssueFromContext(ctx *context.APIContext) *issues_model.Issue {
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
||||||
if err != nil {
|
|
||||||
ctx.APIErrorAuto(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
issue.Repo = ctx.Repo.Repository
|
|
||||||
|
|
||||||
return issue
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
|
func getIssueAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
|
||||||
issue := getIssueFromContext(ctx)
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if issue == nil {
|
if issue == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,11 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
stdCtx "context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
issues_model "gitea.dev/models/issues"
|
issues_model "gitea.dev/models/issues"
|
||||||
"gitea.dev/models/perm"
|
"gitea.dev/models/perm"
|
||||||
access_model "gitea.dev/models/perm/access"
|
|
||||||
repo_model "gitea.dev/models/repo"
|
|
||||||
"gitea.dev/models/unit"
|
"gitea.dev/models/unit"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/optional"
|
"gitea.dev/modules/optional"
|
||||||
@@ -69,17 +66,10 @@ func ListIssueComments(ctx *context.APIContext) {
|
|||||||
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
issue.Repo = ctx.Repo.Repository
|
|
||||||
|
|
||||||
opts := &issues_model.FindCommentsOptions{
|
opts := &issues_model.FindCommentsOptions{
|
||||||
IssueID: issue.ID,
|
IssueID: issue.ID,
|
||||||
@@ -173,12 +163,10 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
|
|||||||
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
issue.Repo = ctx.Repo.Repository
|
|
||||||
|
|
||||||
opts := &issues_model.FindCommentsOptions{
|
opts := &issues_model.FindCommentsOptions{
|
||||||
ListOptions: utils.GetListOptions(ctx),
|
ListOptions: utils.GetListOptions(ctx),
|
||||||
@@ -198,10 +186,14 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
|
|||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if comments, err = issue_service.FilterReadableComments(ctx, ctx.Doer, ctx.Repo.Permission, issue, comments); err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var apiComments []*api.TimelineComment
|
var apiComments []*api.TimelineComment
|
||||||
for _, comment := range comments {
|
for _, comment := range comments {
|
||||||
if comment.Type != issues_model.CommentTypeCode && isXRefCommentAccessible(ctx, ctx.Doer, comment, issue.RepoID) {
|
if comment.Type != issues_model.CommentTypeCode {
|
||||||
comment.Issue = issue
|
comment.Issue = issue
|
||||||
apiComments = append(apiComments, convert.ToTimelineComment(ctx, issue.Repo, comment, ctx.Doer))
|
apiComments = append(apiComments, convert.ToTimelineComment(ctx, issue.Repo, comment, ctx.Doer))
|
||||||
}
|
}
|
||||||
@@ -211,26 +203,6 @@ func ListIssueCommentsAndTimeline(ctx *context.APIContext) {
|
|||||||
ctx.JSON(http.StatusOK, &apiComments)
|
ctx.JSON(http.StatusOK, &apiComments)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isXRefCommentAccessible(ctx stdCtx.Context, user *user_model.User, c *issues_model.Comment, issueRepoID int64) bool {
|
|
||||||
// Remove comments that the user has no permissions to see
|
|
||||||
if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issueRepoID && c.RefRepoID != 0 {
|
|
||||||
var err error
|
|
||||||
// Set RefRepo for description in template
|
|
||||||
c.RefRepo, err = repo_model.GetRepositoryByID(ctx, c.RefRepoID)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
perm, err := access_model.GetDoerRepoPermission(ctx, c.RefRepo, user)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListRepoIssueComments returns all issue-comments for a repo
|
// ListRepoIssueComments returns all issue-comments for a repo
|
||||||
func ListRepoIssueComments(ctx *context.APIContext) {
|
func ListRepoIssueComments(ctx *context.APIContext) {
|
||||||
// swagger:operation GET /repos/{owner}/{repo}/issues/comments issue issueGetRepoComments
|
// swagger:operation GET /repos/{owner}/{repo}/issues/comments issue issueGetRepoComments
|
||||||
@@ -381,19 +353,12 @@ func CreateIssueComment(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
form := web.GetForm[*api.CreateIssueCommentOption](ctx)
|
form := web.GetForm[*api.CreateIssueCommentOption](ctx)
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
if isIssueLockedForDoer(ctx, issue) {
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if issue.IsLocked && !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
|
|
||||||
ctx.APIError(http.StatusForbidden, ctx.Locale.TrString("repo.issues.comment_on_locked"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
perm_model "gitea.dev/models/perm"
|
||||||
|
access_model "gitea.dev/models/perm/access"
|
||||||
|
repo_model "gitea.dev/models/repo"
|
||||||
|
"gitea.dev/models/unit"
|
||||||
|
"gitea.dev/models/unittest"
|
||||||
|
"gitea.dev/services/context"
|
||||||
|
"gitea.dev/services/contexttest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIssueReadEndpointsRequireUnitPermission(t *testing.T) {
|
||||||
|
unittest.PrepareTestEnv(t)
|
||||||
|
|
||||||
|
for name, handler := range map[string]func(*context.APIContext){
|
||||||
|
"timeline": ListIssueCommentsAndTimeline,
|
||||||
|
"labels": ListIssueLabels,
|
||||||
|
"subscribers": GetIssueSubscribers,
|
||||||
|
"attachments": ListIssueAttachments,
|
||||||
|
} {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
request := func(index string) int {
|
||||||
|
ctx, resp := contexttest.MockAPIContext(t, "user2/repo1")
|
||||||
|
contexttest.LoadRepo(t, ctx, 1)
|
||||||
|
ctx.Repo.Permission = access_model.Permission{}
|
||||||
|
ctx.Repo.Permission.SetUnitsWithDefaultAccessMode([]*repo_model.RepoUnit{{Type: unit.TypeIssues}}, perm_model.AccessModeRead)
|
||||||
|
ctx.SetPathParam("index", index)
|
||||||
|
handler(ctx)
|
||||||
|
return resp.Code
|
||||||
|
}
|
||||||
|
assert.Equal(t, http.StatusOK, request("1"))
|
||||||
|
assert.Equal(t, http.StatusNotFound, request("2"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,15 +61,8 @@ func GetIssueDependencies(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorAuto(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. We must be able to read this issue
|
|
||||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +72,7 @@ func GetIssueDependencies(ctx *context.APIContext) {
|
|||||||
|
|
||||||
blockerIssues := make([]*issues_model.Issue, 0, min(listOptions.PageSize, setting.API.MaxResponseItems))
|
blockerIssues := make([]*issues_model.Issue, 0, min(listOptions.PageSize, setting.API.MaxResponseItems))
|
||||||
|
|
||||||
// 2. Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
|
// Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
|
||||||
blockersInfo, total, err := issue.BlockedByDependencies(ctx, listOptions)
|
blockersInfo, total, err := issue.BlockedByDependencies(ctx, listOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
@@ -177,7 +170,7 @@ func CreateIssueDependency(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
|
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
|
||||||
target := getParamsIssue(ctx)
|
target := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -238,7 +231,7 @@ func RemoveIssueDependency(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
|
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
|
||||||
target := getParamsIssue(ctx)
|
target := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -303,16 +296,11 @@ func GetIssueBlocks(ctx *context.APIContext) {
|
|||||||
// We need to list the issues that DEPEND on this issue not the other way round
|
// We need to list the issues that DEPEND on this issue not the other way round
|
||||||
// Therefore whether dependencies are enabled or not in this repository is potentially irrelevant.
|
// Therefore whether dependencies are enabled or not in this repository is potentially irrelevant.
|
||||||
|
|
||||||
issue := getParamsIssue(ctx)
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
page := max(ctx.FormInt("page"), 1)
|
page := max(ctx.FormInt("page"), 1)
|
||||||
limit := ctx.FormInt("limit")
|
limit := ctx.FormInt("limit")
|
||||||
if limit <= 1 {
|
if limit <= 1 {
|
||||||
@@ -399,7 +387,7 @@ func CreateIssueBlocking(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// description: the issue does not exist
|
// description: the issue does not exist
|
||||||
|
|
||||||
dependency := getParamsIssue(ctx)
|
dependency := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -456,7 +444,7 @@ func RemoveIssueBlocking(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
dependency := getParamsIssue(ctx)
|
dependency := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if ctx.Written() {
|
if ctx.Written() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -480,16 +468,6 @@ func RemoveIssueBlocking(ctx *context.APIContext) {
|
|||||||
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
|
ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
|
||||||
if err != nil {
|
|
||||||
ctx.APIErrorAuto(err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
issue.Repo = ctx.Repo.Repository
|
|
||||||
return issue
|
|
||||||
}
|
|
||||||
|
|
||||||
func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Issue {
|
func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Issue {
|
||||||
var repo *repo_model.Repository
|
var repo *repo_model.Repository
|
||||||
if form.Owner != ctx.Repo.Repository.OwnerName || form.Name != ctx.Repo.Repository.Name {
|
if form.Owner != ctx.Repo.Repository.OwnerName || form.Name != ctx.Repo.Repository.Name {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
issues_model "gitea.dev/models/issues"
|
issues_model "gitea.dev/models/issues"
|
||||||
|
"gitea.dev/modules/container"
|
||||||
api "gitea.dev/modules/structs"
|
api "gitea.dev/modules/structs"
|
||||||
"gitea.dev/modules/web"
|
"gitea.dev/modules/web"
|
||||||
"gitea.dev/services/context"
|
"gitea.dev/services/context"
|
||||||
@@ -47,13 +48,8 @@ func ListIssueLabels(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,13 +163,8 @@ func DeleteIssueLabel(ctx *context.APIContext) {
|
|||||||
// "423":
|
// "423":
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,13 +283,8 @@ func ClearIssueLabels(ctx *context.APIContext) {
|
|||||||
// "423":
|
// "423":
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,14 +302,9 @@ func ClearIssueLabels(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (*issues_model.Issue, []*issues_model.Label, error) {
|
func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption) (*issues_model.Issue, []*issues_model.Label, error) {
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
return nil, nil, errors.New("issue not found")
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
|
||||||
@@ -359,12 +340,23 @@ func prepareForReplaceOrAdd(ctx *context.APIContext, form api.IssueLabelsOption)
|
|||||||
}
|
}
|
||||||
labelIDs = append(labelIDs, repoLabelIDs...)
|
labelIDs = append(labelIDs, repoLabelIDs...)
|
||||||
if ctx.Repo.Owner.IsOrganization() {
|
if ctx.Repo.Owner.IsOrganization() {
|
||||||
orgLabelIDs, err := issues_model.GetLabelIDsInOrgByNames(ctx, ctx.Repo.Owner.ID, labelNames)
|
repoLabels, err := issues_model.GetLabelsByIDs(ctx, repoLabelIDs, "name")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
labelIDs = append(labelIDs, orgLabelIDs...)
|
orgLabelNames := container.SetOf(labelNames...)
|
||||||
|
for _, label := range repoLabels {
|
||||||
|
orgLabelNames.Remove(label.Name)
|
||||||
|
}
|
||||||
|
if len(orgLabelNames) > 0 {
|
||||||
|
orgLabelIDs, err := issues_model.GetLabelIDsInOrgByNames(ctx, ctx.Repo.Owner.ID, orgLabelNames.Values())
|
||||||
|
if err != nil {
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
labelIDs = append(labelIDs, orgLabelIDs...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,9 +51,8 @@ func LockIssue(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
reason := web.GetForm[*api.LockIssueOption](ctx).Reason
|
reason := web.GetForm[*api.LockIssueOption](ctx).Reason
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorAuto(err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ func LockIssue(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
issue.Repo = ctx.Repo.Repository
|
issue.Repo = ctx.Repo.Repository
|
||||||
err = issues_model.LockIssue(ctx, opt)
|
err := issues_model.LockIssue(ctx, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
@@ -114,9 +113,8 @@ func UnlockIssue(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorAuto(err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +130,7 @@ func UnlockIssue(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
issue.Repo = ctx.Repo.Repository
|
issue.Repo = ctx.Repo.Repository
|
||||||
err = issues_model.UnlockIssue(ctx, opt)
|
err := issues_model.UnlockIssue(ctx, opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -41,20 +41,13 @@ func PinIssue(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/forbidden"
|
// "$ref": "#/responses/forbidden"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else if issues_model.IsErrIssueMaxPinReached(err) {
|
|
||||||
ctx.APIError(http.StatusBadRequest, err.Error())
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't do this, it will crash when trying to add the pin event to the comment history
|
// If we don't do this, it will crash when trying to add the pin event to the comment history
|
||||||
err = issue.LoadRepo(ctx)
|
err := issue.LoadRepo(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
@@ -98,18 +91,13 @@ func UnpinIssue(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/forbidden"
|
// "$ref": "#/responses/forbidden"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we don't do this, it will crash when trying to add the unpin event to the comment history
|
// If we don't do this, it will crash when trying to add the unpin event to the comment history
|
||||||
err = issue.LoadRepo(ctx)
|
err := issue.LoadRepo(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
@@ -159,17 +147,12 @@ func MoveIssuePin(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/forbidden"
|
// "$ref": "#/responses/forbidden"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = issues_model.MovePin(ctx, issue, ctx.PathParamInt("position"))
|
err := issues_model.MovePin(ctx, issue, ctx.PathParamInt("position"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -172,13 +172,8 @@ func DeleteIssueStopwatch(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func prepareIssueForStopwatch(ctx *context.APIContext) *issues_model.Issue {
|
func prepareIssueForStopwatch(ctx *context.APIContext) *issues_model.Issue {
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,14 +104,8 @@ func DelIssueSubscription(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func setIssueSubscription(ctx *context.APIContext, watch bool) {
|
func setIssueSubscription(ctx *context.APIContext, watch bool) {
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,14 +179,8 @@ func CheckIssueSubscription(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,14 +239,8 @@ func GetIssueSubscribers(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
if issues_model.IsErrIssueNotExist(err) {
|
|
||||||
ctx.APIErrorNotFound()
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,9 +75,8 @@ func ListTrackedTimes(ctx *context.APIContext) {
|
|||||||
ctx.APIErrorNotFound("timetracker is disabled")
|
ctx.APIErrorNotFound("timetracker is disabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorAuto(err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,6 +99,7 @@ func ListTrackedTimes(ctx *context.APIContext) {
|
|||||||
opts.UserID = user.ID
|
opts.UserID = user.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
|
if opts.CreatedBeforeUnix, opts.CreatedAfterUnix, err = context.GetQueryBeforeSince(ctx.Base); err != nil {
|
||||||
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
||||||
return
|
return
|
||||||
@@ -200,9 +200,8 @@ func AddTime(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
form := web.GetForm[*api.AddTimeOption](ctx)
|
form := web.GetForm[*api.AddTimeOption](ctx)
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorAuto(err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +218,7 @@ func AddTime(ctx *context.APIContext) {
|
|||||||
if form.User != "" {
|
if form.User != "" {
|
||||||
if (ctx.IsUserRepoAdmin() && ctx.Doer.Name != form.User) || ctx.Doer.IsAdmin {
|
if (ctx.IsUserRepoAdmin() && ctx.Doer.Name != form.User) || ctx.Doer.IsAdmin {
|
||||||
// allow only RepoAdmin, Admin and User to add time
|
// allow only RepoAdmin, Admin and User to add time
|
||||||
|
var err error
|
||||||
user, err = user_model.GetUserByName(ctx, form.User)
|
user, err = user_model.GetUserByName(ctx, form.User)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.APIErrorAuto(err)
|
ctx.APIErrorAuto(err)
|
||||||
@@ -280,9 +280,8 @@ func ResetIssueTime(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorAuto(err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +294,7 @@ func ResetIssueTime(ctx *context.APIContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = issues_model.DeleteIssueUserTimes(ctx, issue, ctx.Doer)
|
err := issues_model.DeleteIssueUserTimes(ctx, issue, ctx.Doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if db.IsErrNotExist(err) {
|
if db.IsErrNotExist(err) {
|
||||||
ctx.APIError(http.StatusNotFound, err.Error())
|
ctx.APIError(http.StatusNotFound, err.Error())
|
||||||
@@ -349,9 +348,8 @@ func DeleteTime(ctx *context.APIContext) {
|
|||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
|
issue := getRepoIssueByIndexForDoer(ctx, ctx.PathParamInt64("index"))
|
||||||
if err != nil {
|
if issue == nil {
|
||||||
ctx.APIErrorAuto(err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -219,7 +219,10 @@ func EditMilestone(ctx *context.APIContext) {
|
|||||||
if form.Description != nil {
|
if form.Description != nil {
|
||||||
milestone.Content = *form.Description
|
milestone.Content = *form.Description
|
||||||
}
|
}
|
||||||
milestone.DeadlineUnix, _ = common.ParseAPIDeadlineToEndOfDay(form.Deadline)
|
if _, err := common.ApplyAPIDeadlineTimestamp(&milestone.DeadlineUnix, form.Deadline, nil); err != nil {
|
||||||
|
ctx.APIErrorAuto(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
oldIsClosed := milestone.IsClosed
|
oldIsClosed := milestone.IsClosed
|
||||||
if form.State != nil {
|
if form.State != nil {
|
||||||
|
|||||||
@@ -266,6 +266,9 @@ func CreatePullReviewCommentReply(ctx *context.APIContext) {
|
|||||||
ctx.APIError(http.StatusBadRequest, "comment is not a review comment")
|
ctx.APIError(http.StatusBadRequest, "comment is not a review comment")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if isIssueLockedForDoer(ctx, parent.Issue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
comment, err := pull_service.CreateCodeComment(ctx,
|
comment, err := pull_service.CreateCodeComment(ctx,
|
||||||
ctx.Doer, ctx.Repo.GitRepo, parent.Issue,
|
ctx.Doer, ctx.Repo.GitRepo, parent.Issue,
|
||||||
@@ -507,8 +510,12 @@ func CreatePullReview(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// determine review type
|
// determine review type
|
||||||
reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body)
|
reviewType := preparePullReviewType(ctx, pr, opts.Event, opts.Body)
|
||||||
if isWrong {
|
if reviewType == issues_model.ReviewTypeUnknown {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isIssueLockedForDoer(ctx, pr.Issue) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -634,8 +641,12 @@ func SubmitPullReview(ctx *context.APIContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// determine review type
|
// determine review type
|
||||||
reviewType, isWrong := preparePullReviewType(ctx, pr, opts.Event, opts.Body)
|
reviewType := preparePullReviewType(ctx, pr, opts.Event, opts.Body)
|
||||||
if isWrong {
|
if reviewType == issues_model.ReviewTypeUnknown {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isIssueLockedForDoer(ctx, pr.Issue) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,11 +682,18 @@ func SubmitPullReview(ctx *context.APIContext) {
|
|||||||
ctx.JSON(http.StatusOK, apiReview)
|
ctx.JSON(http.StatusOK, apiReview)
|
||||||
}
|
}
|
||||||
|
|
||||||
// preparePullReviewType return ReviewType and false or nil and true if an error happen
|
func isIssueLockedForDoer(ctx *context.APIContext, issue *issues_model.Issue) bool {
|
||||||
func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest, event api.ReviewStateType, body string) (issues_model.ReviewType, bool) {
|
if issue_service.IsLockedForDoer(issue, ctx.Repo.Permission) {
|
||||||
|
ctx.APIError(http.StatusForbidden, "cannot comment on a locked issue")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest, event api.ReviewStateType, body string) issues_model.ReviewType {
|
||||||
if err := pr.LoadIssue(ctx); err != nil {
|
if err := pr.LoadIssue(ctx); err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorInternal(err)
|
||||||
return -1, true
|
return issues_model.ReviewTypeUnknown
|
||||||
}
|
}
|
||||||
|
|
||||||
switch event {
|
switch event {
|
||||||
@@ -683,26 +701,26 @@ func preparePullReviewType(ctx *context.APIContext, pr *issues_model.PullRequest
|
|||||||
// can not approve your own PR
|
// can not approve your own PR
|
||||||
if pr.Issue.IsPoster(ctx.Doer.ID) {
|
if pr.Issue.IsPoster(ctx.Doer.ID) {
|
||||||
ctx.APIError(http.StatusUnprocessableEntity, "approve your own pull is not allowed")
|
ctx.APIError(http.StatusUnprocessableEntity, "approve your own pull is not allowed")
|
||||||
return -1, true
|
return issues_model.ReviewTypeUnknown
|
||||||
}
|
}
|
||||||
return issues_model.ReviewTypeApprove, false
|
return issues_model.ReviewTypeApprove
|
||||||
|
|
||||||
case api.ReviewStateRequestChanges:
|
case api.ReviewStateRequestChanges:
|
||||||
// can not reject your own PR
|
// can not reject your own PR
|
||||||
if pr.Issue.IsPoster(ctx.Doer.ID) {
|
if pr.Issue.IsPoster(ctx.Doer.ID) {
|
||||||
ctx.APIError(http.StatusUnprocessableEntity, "reject your own pull is not allowed")
|
ctx.APIError(http.StatusUnprocessableEntity, "reject your own pull is not allowed")
|
||||||
return -1, true
|
return issues_model.ReviewTypeUnknown
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(body) == "" {
|
if strings.TrimSpace(body) == "" {
|
||||||
ctx.APIError(http.StatusUnprocessableEntity, "review event REQUEST_CHANGES requires a body")
|
ctx.APIError(http.StatusUnprocessableEntity, "review event REQUEST_CHANGES requires a body")
|
||||||
return -1, true
|
return issues_model.ReviewTypeUnknown
|
||||||
}
|
}
|
||||||
return issues_model.ReviewTypeReject, false
|
return issues_model.ReviewTypeReject
|
||||||
|
|
||||||
case api.ReviewStateComment:
|
case api.ReviewStateComment:
|
||||||
return issues_model.ReviewTypeComment, false
|
return issues_model.ReviewTypeComment
|
||||||
default:
|
default:
|
||||||
return issues_model.ReviewTypePending, false
|
return issues_model.ReviewTypePending
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import (
|
|||||||
unit_model "gitea.dev/models/unit"
|
unit_model "gitea.dev/models/unit"
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
"gitea.dev/modules/git"
|
"gitea.dev/modules/git"
|
||||||
"gitea.dev/modules/label"
|
|
||||||
"gitea.dev/modules/log"
|
"gitea.dev/modules/log"
|
||||||
"gitea.dev/modules/markup"
|
"gitea.dev/modules/markup"
|
||||||
"gitea.dev/modules/optional"
|
"gitea.dev/modules/optional"
|
||||||
@@ -255,17 +254,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
|
|||||||
ObjectFormatName: string(opt.ObjectFormatName),
|
ObjectFormatName: string(opt.ObjectFormatName),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if repo_model.IsErrRepoAlreadyExist(err) {
|
ctx.APIErrorAuto(err)
|
||||||
ctx.APIError(http.StatusConflict, "The repository with the same name already exists.")
|
|
||||||
} else if db.IsErrNameReserved(err) ||
|
|
||||||
db.IsErrNamePatternNotAllowed(err) ||
|
|
||||||
label.IsErrTemplateLoad(err) {
|
|
||||||
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
|
||||||
} else if errors.Is(err, util.ErrPermissionDenied) {
|
|
||||||
ctx.APIError(http.StatusForbidden, err.Error())
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
user_model "gitea.dev/models/user"
|
user_model "gitea.dev/models/user"
|
||||||
@@ -212,7 +211,6 @@ func CheckFollowing(ctx *context.APIContext) {
|
|||||||
checkUserFollowing(ctx, ctx.ContextUser, target.ID)
|
checkUserFollowing(ctx, ctx.ContextUser, target.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Follow follow a user
|
|
||||||
func Follow(ctx *context.APIContext) {
|
func Follow(ctx *context.APIContext) {
|
||||||
// swagger:operation PUT /user/following/{username} user userCurrentPutFollow
|
// swagger:operation PUT /user/following/{username} user userCurrentPutFollow
|
||||||
// ---
|
// ---
|
||||||
@@ -232,17 +230,12 @@ func Follow(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil {
|
if err := user_model.FollowUser(ctx, ctx.Doer, ctx.ContextUser); err != nil {
|
||||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
ctx.APIErrorAuto(err)
|
||||||
ctx.APIError(http.StatusForbidden, err.Error())
|
|
||||||
} else {
|
|
||||||
ctx.APIErrorInternal(err)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Status(http.StatusNoContent)
|
ctx.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unfollow unfollow a user
|
|
||||||
func Unfollow(ctx *context.APIContext) {
|
func Unfollow(ctx *context.APIContext) {
|
||||||
// swagger:operation DELETE /user/following/{username} user userCurrentDeleteFollow
|
// swagger:operation DELETE /user/following/{username} user userCurrentDeleteFollow
|
||||||
// ---
|
// ---
|
||||||
@@ -260,7 +253,7 @@ func Unfollow(ctx *context.APIContext) {
|
|||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
if err := user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
if err := user_model.UnfollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIErrorAuto(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Status(http.StatusNoContent)
|
ctx.Status(http.StatusNoContent)
|
||||||
|
|||||||
Loaded 100 of 204 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user