forked from NYANDEV/forgejo
Merge branch 'rebase-forgejo-dependency' into forgejo
This commit is contained in:
commit
e165ff8886
221 changed files with 7000 additions and 622 deletions
|
@ -103,6 +103,8 @@ package "code.gitea.io/gitea/models/unittest"
|
||||||
func LoadFixtures
|
func LoadFixtures
|
||||||
func Copy
|
func Copy
|
||||||
func CopyDir
|
func CopyDir
|
||||||
|
func NewMockWebServer
|
||||||
|
func NormalizedFullPath
|
||||||
func FixturesDir
|
func FixturesDir
|
||||||
func fatalTestError
|
func fatalTestError
|
||||||
func InitSettings
|
func InitSettings
|
||||||
|
|
10
assets/go-licenses.json
generated
10
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"go.buildTags": "'sqlite sqlite_unlock_notify'",
|
"go.buildTags": "sqlite,sqlite_unlock_notify",
|
||||||
"go.testFlags": ["-v"]
|
"go.testFlags": ["-v"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -412,6 +412,10 @@ USER = root
|
||||||
;;
|
;;
|
||||||
;; Whether execute database models migrations automatically
|
;; Whether execute database models migrations automatically
|
||||||
;AUTO_MIGRATION = true
|
;AUTO_MIGRATION = true
|
||||||
|
;;
|
||||||
|
;; Threshold value (in seconds) beyond which query execution time is logged as a warning in the xorm logger
|
||||||
|
;;
|
||||||
|
;SLOW_QUERY_TRESHOLD = 5s
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -817,6 +821,11 @@ LEVEL = Info
|
||||||
;; Every new user will have restricted permissions depending on this setting
|
;; Every new user will have restricted permissions depending on this setting
|
||||||
;DEFAULT_USER_IS_RESTRICTED = false
|
;DEFAULT_USER_IS_RESTRICTED = false
|
||||||
;;
|
;;
|
||||||
|
;; Users will be able to use dots when choosing their username. Disabling this is
|
||||||
|
;; helpful if your usersare having issues with e.g. RSS feeds or advanced third-party
|
||||||
|
;; extensions that use strange regex patterns.
|
||||||
|
; ALLOW_DOTS_IN_USERNAMES = true
|
||||||
|
;;
|
||||||
;; Either "public", "limited" or "private", default is "public"
|
;; Either "public", "limited" or "private", default is "public"
|
||||||
;; Limited is for users visible only to signed users
|
;; Limited is for users visible only to signed users
|
||||||
;; Private is for users visible only to members of their organizations
|
;; Private is for users visible only to members of their organizations
|
||||||
|
@ -903,6 +912,14 @@ LEVEL = Info
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;[badges]
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; Enable repository badges (via shields.io or a similar generator)
|
||||||
|
;ENABLED = true
|
||||||
|
;; Template for the badge generator.
|
||||||
|
;GENERATOR_URL_TEMPLATE = https://img.shields.io/badge/{{.label}}-{{.text}}-{{.color}}
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;[repository]
|
;[repository]
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -1467,6 +1484,8 @@ LEVEL = Info
|
||||||
;;
|
;;
|
||||||
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
||||||
;DEFAULT_EMAIL_NOTIFICATIONS = enabled
|
;DEFAULT_EMAIL_NOTIFICATIONS = enabled
|
||||||
|
;; Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false
|
||||||
|
;SEND_NOTIFICATION_EMAIL_ON_NEW_USER = false
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -1780,9 +1799,6 @@ LEVEL = Info
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;
|
;;
|
||||||
;AVATAR_UPLOAD_PATH = data/avatars
|
|
||||||
;REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars
|
|
||||||
;;
|
|
||||||
;; How Gitea deals with missing repository avatars
|
;; How Gitea deals with missing repository avatars
|
||||||
;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used
|
;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used
|
||||||
;REPOSITORY_AVATAR_FALLBACK = none
|
;REPOSITORY_AVATAR_FALLBACK = none
|
||||||
|
|
|
@ -457,6 +457,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
|
||||||
- `MAX_IDLE_CONNS` **2**: Max idle database connections on connection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`.
|
- `MAX_IDLE_CONNS` **2**: Max idle database connections on connection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`.
|
||||||
- `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071).
|
- `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071).
|
||||||
- `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically.
|
- `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically.
|
||||||
|
- `SLOW_QUERY_TRESHOLD` **5s**: Threshold value in seconds beyond which query execution time is logged as a warning in the xorm logger.
|
||||||
|
|
||||||
[^1]: It may be necessary to specify a hostport even when listening on a unix socket, as the port is part of the socket name. see [#24552](https://github.com/go-gitea/gitea/issues/24552#issuecomment-1681649367) for additional details.
|
[^1]: It may be necessary to specify a hostport even when listening on a unix socket, as the port is part of the socket name. see [#24552](https://github.com/go-gitea/gitea/issues/24552#issuecomment-1681649367) for additional details.
|
||||||
|
|
||||||
|
@ -516,6 +517,7 @@ And the following unique queues:
|
||||||
|
|
||||||
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
||||||
- `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations.
|
- `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations.
|
||||||
|
- `SEND_NOTIFICATION_EMAIL_ON_NEW_USER`: **false**: Send an email to all admins when a new user signs up to inform the admins about this act.
|
||||||
|
|
||||||
## Security (`security`)
|
## Security (`security`)
|
||||||
|
|
||||||
|
|
8
go.mod
8
go.mod
|
@ -15,7 +15,6 @@ require (
|
||||||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
||||||
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
|
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
|
||||||
github.com/PuerkitoBio/goquery v1.8.1
|
github.com/PuerkitoBio/goquery v1.8.1
|
||||||
github.com/alecthomas/chroma/v2 v2.12.0
|
github.com/alecthomas/chroma/v2 v2.12.0
|
||||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||||
|
@ -77,14 +76,12 @@ require (
|
||||||
github.com/mholt/archiver/v3 v3.5.1
|
github.com/mholt/archiver/v3 v3.5.1
|
||||||
github.com/microcosm-cc/bluemonday v1.0.26
|
github.com/microcosm-cc/bluemonday v1.0.26
|
||||||
github.com/minio/minio-go/v7 v7.0.66
|
github.com/minio/minio-go/v7 v7.0.66
|
||||||
github.com/minio/sha256-simd v1.0.1
|
|
||||||
github.com/msteinert/pam v1.2.0
|
github.com/msteinert/pam v1.2.0
|
||||||
github.com/nektos/act v0.2.52
|
github.com/nektos/act v0.2.52
|
||||||
github.com/niklasfasching/go-org v1.7.0
|
github.com/niklasfasching/go-org v1.7.0
|
||||||
github.com/olivere/elastic/v7 v7.0.32
|
github.com/olivere/elastic/v7 v7.0.32
|
||||||
github.com/opencontainers/go-digest v1.0.0
|
github.com/opencontainers/go-digest v1.0.0
|
||||||
github.com/opencontainers/image-spec v1.1.0-rc5
|
github.com/opencontainers/image-spec v1.1.0-rc5
|
||||||
github.com/pkg/errors v0.9.1
|
|
||||||
github.com/pquerna/otp v1.4.0
|
github.com/pquerna/otp v1.4.0
|
||||||
github.com/prometheus/client_golang v1.17.0
|
github.com/prometheus/client_golang v1.17.0
|
||||||
github.com/quasoft/websspi v1.1.2
|
github.com/quasoft/websspi v1.1.2
|
||||||
|
@ -100,7 +97,6 @@ require (
|
||||||
github.com/ulikunitz/xz v0.5.11
|
github.com/ulikunitz/xz v0.5.11
|
||||||
github.com/urfave/cli/v2 v2.26.0
|
github.com/urfave/cli/v2 v2.26.0
|
||||||
github.com/xanzy/go-gitlab v0.95.2
|
github.com/xanzy/go-gitlab v0.95.2
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0
|
|
||||||
github.com/yohcop/openid-go v1.0.1
|
github.com/yohcop/openid-go v1.0.1
|
||||||
github.com/yuin/goldmark v1.6.0
|
github.com/yuin/goldmark v1.6.0
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
|
@ -232,6 +228,7 @@ require (
|
||||||
github.com/mholt/acmez v1.2.0 // indirect
|
github.com/mholt/acmez v1.2.0 // indirect
|
||||||
github.com/miekg/dns v1.1.57 // indirect
|
github.com/miekg/dns v1.1.57 // indirect
|
||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
|
@ -247,6 +244,7 @@ require (
|
||||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.19 // indirect
|
github.com/pierrec/lz4/v4 v4.1.19 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.45.0 // indirect
|
github.com/prometheus/common v0.45.0 // indirect
|
||||||
|
@ -277,8 +275,6 @@ require (
|
||||||
github.com/valyala/fastjson v1.6.4 // indirect
|
github.com/valyala/fastjson v1.6.4 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
||||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||||
|
|
9
go.sum
9
go.sum
|
@ -93,8 +93,6 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa
|
||||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
|
||||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
|
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||||
|
@ -839,13 +837,6 @@ github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23n
|
||||||
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
|
||||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||||
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
|
||||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
|
||||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
|
||||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
|
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
|
||||||
|
|
|
@ -309,6 +309,32 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
|
||||||
return commiter.Commit()
|
return commiter.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) {
|
||||||
|
var run ActionRun
|
||||||
|
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).OrderBy("id DESC").Limit(1).Get(&run)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, fmt.Errorf("latest run: %w", util.ErrNotExist)
|
||||||
|
}
|
||||||
|
return &run, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLatestRunForBranchAndWorkflow(ctx context.Context, repoID int64, branch, workflowFile, event string) (*ActionRun, error) {
|
||||||
|
var run ActionRun
|
||||||
|
q := db.GetEngine(ctx).Where("repo_id=?", repoID).And("ref=?", branch).And("workflow_id=?", workflowFile)
|
||||||
|
if event != "" {
|
||||||
|
q = q.And("event=?", event)
|
||||||
|
}
|
||||||
|
has, err := q.Desc("id").Get(&run)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if !has {
|
||||||
|
return nil, util.NewNotExistErrorf("run with repo_id %d, ref %s, workflow_id %s", repoID, branch, workflowFile)
|
||||||
|
}
|
||||||
|
return &run, nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) {
|
func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) {
|
||||||
var run ActionRun
|
var run ActionRun
|
||||||
has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run)
|
has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run)
|
||||||
|
|
|
@ -14,6 +14,7 @@ func TestMain(m *testing.M) {
|
||||||
FixtureFiles: []string{
|
FixtureFiles: []string{
|
||||||
"gpg_key.yml",
|
"gpg_key.yml",
|
||||||
"public_key.yml",
|
"public_key.yml",
|
||||||
|
"TestParseCommitWithSSHSignature/public_key.yml",
|
||||||
"deploy_key.yml",
|
"deploy_key.yml",
|
||||||
"gpg_key_import.yml",
|
"gpg_key_import.yml",
|
||||||
"user.yml",
|
"user.yml",
|
||||||
|
|
|
@ -169,7 +169,12 @@ func RewriteAllPublicKeys(ctx context.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Close()
|
if err := t.Sync(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := t.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return util.Rename(tmpPath, fPath)
|
return util.Rename(tmpPath, fPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,7 +92,12 @@ func RewriteAllPrincipalKeys(ctx context.Context) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Close()
|
if err := t.Sync(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := t.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return util.Rename(tmpPath, fPath)
|
return util.Rename(tmpPath, fPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,12 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
|
||||||
log.Error("GetEmailAddresses: %v", err)
|
log.Error("GetEmailAddresses: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add the noreply email address as verified address.
|
||||||
|
committerEmailAddresses = append(committerEmailAddresses, &user_model.EmailAddress{
|
||||||
|
IsActivated: true,
|
||||||
|
Email: committer.GetPlaceholderEmail(),
|
||||||
|
})
|
||||||
|
|
||||||
activated := false
|
activated := false
|
||||||
for _, e := range committerEmailAddresses {
|
for _, e := range committerEmailAddresses {
|
||||||
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
|
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
|
||||||
|
|
146
models/asymkey/ssh_key_commit_verification_test.go
Normal file
146
models/asymkey/ssh_key_commit_verification_test.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package asymkey
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseCommitWithSSHSignature(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
sshKey := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1000, OwnerID: 2})
|
||||||
|
|
||||||
|
t.Run("No commiter", func(t *testing.T) {
|
||||||
|
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{}, &user_model.User{})
|
||||||
|
assert.False(t, commitVerification.Verified)
|
||||||
|
assert.Equal(t, NoKeyFound, commitVerification.Reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Commiter without keys", func(t *testing.T) {
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||||
|
|
||||||
|
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{Committer: &git.Signature{Email: user.Email}}, user)
|
||||||
|
assert.False(t, commitVerification.Verified)
|
||||||
|
assert.Equal(t, NoKeyFound, commitVerification.Reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Correct signature with wrong email", func(t *testing.T) {
|
||||||
|
gitCommit := &git.Commit{
|
||||||
|
Committer: &git.Signature{
|
||||||
|
Email: "non-existent",
|
||||||
|
},
|
||||||
|
Signature: &git.CommitGPGSignature{
|
||||||
|
Payload: `tree 2d491b2985a7ff848d5c02748e7ea9f9f7619f9f
|
||||||
|
parent 45b03601635a1f463b81963a4022c7f87ce96ef9
|
||||||
|
author user2 <non-existent> 1699710556 +0100
|
||||||
|
committer user2 <non-existent> 1699710556 +0100
|
||||||
|
|
||||||
|
Using email that isn't known to Forgejo
|
||||||
|
`,
|
||||||
|
Signature: `-----BEGIN SSH SIGNATURE-----
|
||||||
|
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95
|
||||||
|
f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||||
|
AAAAQIMufOuSjZeDUujrkVK4sl7ICa0WwEftas8UAYxx0Thdkiw2qWjR1U1PKfTLm16/w8
|
||||||
|
/bS1LX1lZNuzm2LR2qEgw=
|
||||||
|
-----END SSH SIGNATURE-----
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
|
||||||
|
assert.False(t, commitVerification.Verified)
|
||||||
|
assert.Equal(t, NoKeyFound, commitVerification.Reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Incorrect signature with correct email", func(t *testing.T) {
|
||||||
|
gitCommit := &git.Commit{
|
||||||
|
Committer: &git.Signature{
|
||||||
|
Email: "user2@example.com",
|
||||||
|
},
|
||||||
|
Signature: &git.CommitGPGSignature{
|
||||||
|
Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f
|
||||||
|
parent c2780d5c313da2a947eae22efd7dacf4213f4e7f
|
||||||
|
author user2 <user2@example.com> 1699707877 +0100
|
||||||
|
committer user2 <user2@example.com> 1699707877 +0100
|
||||||
|
|
||||||
|
Add content
|
||||||
|
`,
|
||||||
|
Signature: `-----BEGIN SSH SIGNATURE-----`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
|
||||||
|
assert.False(t, commitVerification.Verified)
|
||||||
|
assert.Equal(t, NoKeyFound, commitVerification.Reason)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Valid signature with correct email", func(t *testing.T) {
|
||||||
|
gitCommit := &git.Commit{
|
||||||
|
Committer: &git.Signature{
|
||||||
|
Email: "user2@example.com",
|
||||||
|
},
|
||||||
|
Signature: &git.CommitGPGSignature{
|
||||||
|
Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f
|
||||||
|
parent c2780d5c313da2a947eae22efd7dacf4213f4e7f
|
||||||
|
author user2 <user2@example.com> 1699707877 +0100
|
||||||
|
committer user2 <user2@example.com> 1699707877 +0100
|
||||||
|
|
||||||
|
Add content
|
||||||
|
`,
|
||||||
|
Signature: `-----BEGIN SSH SIGNATURE-----
|
||||||
|
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95
|
||||||
|
f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||||
|
AAAAQBe2Fwk/FKY3SBCnG6jSYcO6ucyahp2SpQ/0P+otslzIHpWNW8cQ0fGLdhhaFynJXQ
|
||||||
|
fs9cMpZVM9BfIKNUSO8QY=
|
||||||
|
-----END SSH SIGNATURE-----
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
|
||||||
|
assert.True(t, commitVerification.Verified)
|
||||||
|
assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason)
|
||||||
|
assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Valid signature with noreply email", func(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "noreply.example.com")()
|
||||||
|
|
||||||
|
gitCommit := &git.Commit{
|
||||||
|
Committer: &git.Signature{
|
||||||
|
Email: "user2@noreply.example.com",
|
||||||
|
},
|
||||||
|
Signature: &git.CommitGPGSignature{
|
||||||
|
Payload: `tree 4836c7f639f37388bab4050ef5c97bbbd54272fc
|
||||||
|
parent 795be1b0117ea5c65456050bb9fd84744d4fd9c6
|
||||||
|
author user2 <user2@noreply.example.com> 1699709594 +0100
|
||||||
|
committer user2 <user2@noreply.example.com> 1699709594 +0100
|
||||||
|
|
||||||
|
Commit with noreply
|
||||||
|
`,
|
||||||
|
Signature: `-----BEGIN SSH SIGNATURE-----
|
||||||
|
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95
|
||||||
|
f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||||
|
AAAAQJz83KKxD6Bz/ZvNpqkA3RPOSQ4LQ5FfEItbtoONkbwV9wAWMnmBqgggo/lnXCJ3oq
|
||||||
|
muPLbvEduU+Ze/1Ol1pgk=
|
||||||
|
-----END SSH SIGNATURE-----
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
|
||||||
|
assert.True(t, commitVerification.Verified)
|
||||||
|
assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason)
|
||||||
|
assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
|
||||||
|
})
|
||||||
|
}
|
|
@ -250,7 +250,7 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
|
||||||
remainingScopes = remainingScopes[i+1:]
|
remainingScopes = remainingScopes[i+1:]
|
||||||
}
|
}
|
||||||
singleScope := AccessTokenScope(v)
|
singleScope := AccessTokenScope(v)
|
||||||
if singleScope == "" {
|
if singleScope == "" || singleScope == "sudo" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if singleScope == AccessTokenScopeAll {
|
if singleScope == AccessTokenScopeAll {
|
||||||
|
|
|
@ -20,7 +20,7 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
|
||||||
tests := []scopeTestNormalize{
|
tests := []scopeTestNormalize{
|
||||||
{"", "", nil},
|
{"", "", nil},
|
||||||
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
|
{"write:misc,write:notification,read:package,write:notification,public-only", "public-only,write:misc,write:notification,read:package", nil},
|
||||||
{"all", "all", nil},
|
{"all,sudo", "all", nil},
|
||||||
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil},
|
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user", "all", nil},
|
||||||
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
|
{"write:activitypub,write:admin,write:misc,write:notification,write:organization,write:package,write:issue,write:repository,write:user,public-only", "public-only,all", nil},
|
||||||
}
|
}
|
||||||
|
|
142
models/auth/session_test.go
Normal file
142
models/auth/session_test.go
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/auth"
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAuthSession(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
defer timeutil.MockUnset()
|
||||||
|
|
||||||
|
key := "I-Like-Free-Software"
|
||||||
|
|
||||||
|
t.Run("Create Session", func(t *testing.T) {
|
||||||
|
// Ensure it doesn't exist.
|
||||||
|
ok, err := auth.ExistSession(db.DefaultContext, key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
preCount, err := auth.CountSessions(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
now := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
timeutil.MockSet(now)
|
||||||
|
|
||||||
|
// New session is created.
|
||||||
|
sess, err := auth.ReadSession(db.DefaultContext, key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, key, sess.Key)
|
||||||
|
assert.Empty(t, sess.Data)
|
||||||
|
assert.EqualValues(t, now.Unix(), sess.Expiry)
|
||||||
|
|
||||||
|
// Ensure it exists.
|
||||||
|
ok, err = auth.ExistSession(db.DefaultContext, key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// Ensure the session is taken into account for count..
|
||||||
|
postCount, err := auth.CountSessions(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Greater(t, postCount, preCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Update session", func(t *testing.T) {
|
||||||
|
data := []byte{0xba, 0xdd, 0xc0, 0xde}
|
||||||
|
now := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
timeutil.MockSet(now)
|
||||||
|
|
||||||
|
// Update session.
|
||||||
|
err := auth.UpdateSession(db.DefaultContext, key, data)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
timeutil.MockSet(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
// Read updated session.
|
||||||
|
// Ensure data is updated and expiry is set from the update session call.
|
||||||
|
sess, err := auth.ReadSession(db.DefaultContext, key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.EqualValues(t, key, sess.Key)
|
||||||
|
assert.EqualValues(t, data, sess.Data)
|
||||||
|
assert.EqualValues(t, now.Unix(), sess.Expiry)
|
||||||
|
|
||||||
|
timeutil.MockSet(now)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Delete session", func(t *testing.T) {
|
||||||
|
// Ensure it't exist.
|
||||||
|
ok, err := auth.ExistSession(db.DefaultContext, key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
preCount, err := auth.CountSessions(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = auth.DestroySession(db.DefaultContext, key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Ensure it doens't exists.
|
||||||
|
ok, err = auth.ExistSession(db.DefaultContext, key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
// Ensure the session is taken into account for count..
|
||||||
|
postCount, err := auth.CountSessions(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Less(t, postCount, preCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Cleanup sessions", func(t *testing.T) {
|
||||||
|
timeutil.MockSet(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||||
|
|
||||||
|
_, err := auth.ReadSession(db.DefaultContext, "sess-1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// One minute later.
|
||||||
|
timeutil.MockSet(time.Date(2023, 1, 1, 0, 1, 0, 0, time.UTC))
|
||||||
|
_, err = auth.ReadSession(db.DefaultContext, "sess-2")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// 5 minutes, shouldn't clean up anything.
|
||||||
|
err = auth.CleanupSessions(db.DefaultContext, 5*60)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
ok, err := auth.ExistSession(db.DefaultContext, "sess-1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
ok, err = auth.ExistSession(db.DefaultContext, "sess-2")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// 1 minute, should clean up sess-1.
|
||||||
|
err = auth.CleanupSessions(db.DefaultContext, 60)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
ok, err = auth.ExistSession(db.DefaultContext, "sess-1")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
ok, err = auth.ExistSession(db.DefaultContext, "sess-2")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// Now, should clean up sess-2.
|
||||||
|
err = auth.CleanupSessions(db.DefaultContext, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
ok, err = auth.ExistSession(db.DefaultContext, "sess-2")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, ok)
|
||||||
|
})
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ package auth
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
@ -18,7 +19,6 @@ import (
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"golang.org/x/crypto/pbkdf2"
|
"golang.org/x/crypto/pbkdf2"
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,10 +11,13 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
|
"xorm.io/xorm/contexts"
|
||||||
"xorm.io/xorm/names"
|
"xorm.io/xorm/names"
|
||||||
"xorm.io/xorm/schemas"
|
"xorm.io/xorm/schemas"
|
||||||
|
|
||||||
|
@ -144,6 +147,16 @@ func InitEngine(ctx context.Context) error {
|
||||||
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
||||||
xormEngine.SetDefaultContext(ctx)
|
xormEngine.SetDefaultContext(ctx)
|
||||||
|
|
||||||
|
if setting.Database.SlowQueryTreshold > 0 {
|
||||||
|
xormEngine.AddHook(&SlowQueryHook{
|
||||||
|
Treshold: setting.Database.SlowQueryTreshold,
|
||||||
|
Logger: log.GetLogger("xorm"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
xormEngine.AddHook(&ErrorQueryHook{
|
||||||
|
Logger: log.GetLogger("xorm"),
|
||||||
|
})
|
||||||
|
|
||||||
SetDefaultEngine(ctx, xormEngine)
|
SetDefaultEngine(ctx, xormEngine)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -299,3 +312,38 @@ func SetLogSQL(ctx context.Context, on bool) {
|
||||||
sess.Engine().ShowSQL(on)
|
sess.Engine().ShowSQL(on)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SlowQueryHook struct {
|
||||||
|
Treshold time.Duration
|
||||||
|
Logger log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ contexts.Hook = &SlowQueryHook{}
|
||||||
|
|
||||||
|
func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||||
|
return c.Ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||||
|
if c.ExecuteTime >= h.Treshold {
|
||||||
|
h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorQueryHook struct {
|
||||||
|
Logger log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ contexts.Hook = &ErrorQueryHook{}
|
||||||
|
|
||||||
|
func (ErrorQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||||
|
return c.Ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||||
|
if c.Err != nil {
|
||||||
|
h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -6,15 +6,19 @@ package db_test
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
_ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys
|
_ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDumpDatabase(t *testing.T) {
|
func TestDumpDatabase(t *testing.T) {
|
||||||
|
@ -85,3 +89,65 @@ func TestPrimaryKeys(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSlowQuery(t *testing.T) {
|
||||||
|
lc, cleanup := test.NewLogChecker("slow-query")
|
||||||
|
lc.StopMark("[Slow SQL Query]")
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
e := db.GetEngine(db.DefaultContext)
|
||||||
|
engine, ok := e.(*xorm.Engine)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// It's not possible to clean this up with XORM, but it's luckily not harmful
|
||||||
|
// to leave around.
|
||||||
|
engine.AddHook(&db.SlowQueryHook{
|
||||||
|
Treshold: time.Second * 10,
|
||||||
|
Logger: log.GetLogger("slow-query"),
|
||||||
|
})
|
||||||
|
|
||||||
|
// NOOP query.
|
||||||
|
e.Exec("SELECT 1 WHERE false;")
|
||||||
|
|
||||||
|
_, stopped := lc.Check(100 * time.Millisecond)
|
||||||
|
assert.False(t, stopped)
|
||||||
|
|
||||||
|
engine.AddHook(&db.SlowQueryHook{
|
||||||
|
Treshold: 0, // Every query should be logged.
|
||||||
|
Logger: log.GetLogger("slow-query"),
|
||||||
|
})
|
||||||
|
|
||||||
|
// NOOP query.
|
||||||
|
e.Exec("SELECT 1 WHERE false;")
|
||||||
|
|
||||||
|
_, stopped = lc.Check(100 * time.Millisecond)
|
||||||
|
assert.True(t, stopped)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorQuery(t *testing.T) {
|
||||||
|
lc, cleanup := test.NewLogChecker("error-query")
|
||||||
|
lc.StopMark("[Error SQL Query]")
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
e := db.GetEngine(db.DefaultContext)
|
||||||
|
engine, ok := e.(*xorm.Engine)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// It's not possible to clean this up with XORM, but it's luckily not harmful
|
||||||
|
// to leave around.
|
||||||
|
engine.AddHook(&db.ErrorQueryHook{
|
||||||
|
Logger: log.GetLogger("error-query"),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Valid query.
|
||||||
|
e.Exec("SELECT 1 WHERE false;")
|
||||||
|
|
||||||
|
_, stopped := lc.Check(100 * time.Millisecond)
|
||||||
|
assert.False(t, stopped)
|
||||||
|
|
||||||
|
// Table doesn't exist.
|
||||||
|
e.Exec("SELECT column FROM table;")
|
||||||
|
|
||||||
|
_, stopped = lc.Check(100 * time.Millisecond)
|
||||||
|
assert.True(t, stopped)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
-
|
||||||
|
id: 1000
|
||||||
|
owner_id: 2
|
||||||
|
name: user2@localhost
|
||||||
|
fingerprint: "SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4"
|
||||||
|
content: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBknvWcuxM/W0iXGkzY4f2O6feX+Q7o46pKcxUbcOgh user2@localhost"
|
||||||
|
# private key (base64-ed) LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNDZ1pKNzFuTHNUUDF0SWx4cE0yT0g5anVuM2wva082T09xU25NVkczRG9JUUFBQUpocG43YTZhWisyCnVnQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQ2daSjcxbkxzVFAxdElseHBNMk9IOWp1bjNsL2tPNk9PcVNuTVZHM0RvSVEKQUFBRUFxVm12bmo1LzZ5TW12ck9Ub29xa3F5MmUrc21aK0tBcEtKR0crRnY5MlA2QmtudldjdXhNL1cwaVhHa3pZNGYyTwo2ZmVYK1E3bzQ2cEtjeFViY09naEFBQUFFMmQxYzNSbFpFQm5kWE4wWldRdFltVmhjM1FCQWc9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0=
|
||||||
|
mode: 2
|
||||||
|
type: 1
|
||||||
|
verified: true
|
||||||
|
created_unix: 1559593109
|
||||||
|
updated_unix: 1565224552
|
||||||
|
login_source_id: 0
|
|
@ -150,3 +150,17 @@
|
||||||
is_prerelease: false
|
is_prerelease: false
|
||||||
is_tag: false
|
is_tag: false
|
||||||
created_unix: 946684803
|
created_unix: 946684803
|
||||||
|
|
||||||
|
- id: 12
|
||||||
|
repo_id: 59
|
||||||
|
publisher_id: 2
|
||||||
|
tag_name: "v1.0"
|
||||||
|
lower_tag_name: "v1.0"
|
||||||
|
target: "main"
|
||||||
|
title: "v1.0"
|
||||||
|
sha1: "d8f53dfb33f6ccf4169c34970b5e747511c18beb"
|
||||||
|
num_commits: 1
|
||||||
|
is_draft: false
|
||||||
|
is_prerelease: false
|
||||||
|
is_tag: false
|
||||||
|
created_unix: 946684803
|
||||||
|
|
|
@ -608,6 +608,38 @@
|
||||||
type: 1
|
type: 1
|
||||||
created_unix: 946684810
|
created_unix: 946684810
|
||||||
|
|
||||||
|
# BEGIN Forgejo [GITEA] Improve HTML title on repositories
|
||||||
|
-
|
||||||
|
id: 1093
|
||||||
|
repo_id: 59
|
||||||
|
type: 1
|
||||||
|
created_unix: 946684810
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 1094
|
||||||
|
repo_id: 59
|
||||||
|
type: 2
|
||||||
|
created_unix: 946684810
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 1095
|
||||||
|
repo_id: 59
|
||||||
|
type: 3
|
||||||
|
created_unix: 946684810
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 1096
|
||||||
|
repo_id: 59
|
||||||
|
type: 4
|
||||||
|
created_unix: 946684810
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 1097
|
||||||
|
repo_id: 59
|
||||||
|
type: 5
|
||||||
|
created_unix: 946684810
|
||||||
|
# END Forgejo [GITEA] Improve HTML title on repositories
|
||||||
|
|
||||||
-
|
-
|
||||||
id: 91
|
id: 91
|
||||||
repo_id: 58
|
repo_id: 58
|
||||||
|
|
|
@ -1467,6 +1467,7 @@
|
||||||
owner_name: user27
|
owner_name: user27
|
||||||
lower_name: repo49
|
lower_name: repo49
|
||||||
name: repo49
|
name: repo49
|
||||||
|
description: A wonderful repository with more than just a README.md
|
||||||
default_branch: master
|
default_branch: master
|
||||||
num_watches: 0
|
num_watches: 0
|
||||||
num_stars: 0
|
num_stars: 0
|
||||||
|
@ -1693,3 +1694,16 @@
|
||||||
size: 0
|
size: 0
|
||||||
is_fsck_enabled: true
|
is_fsck_enabled: true
|
||||||
close_issues_via_commit_in_any_branch: false
|
close_issues_via_commit_in_any_branch: false
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 59
|
||||||
|
owner_id: 2
|
||||||
|
owner_name: user2
|
||||||
|
lower_name: repo59
|
||||||
|
name: repo59
|
||||||
|
default_branch: master
|
||||||
|
is_empty: false
|
||||||
|
is_archived: false
|
||||||
|
is_private: false
|
||||||
|
status: 0
|
||||||
|
num_issues: 0
|
||||||
|
|
|
@ -66,7 +66,7 @@
|
||||||
num_followers: 2
|
num_followers: 2
|
||||||
num_following: 1
|
num_following: 1
|
||||||
num_stars: 2
|
num_stars: 2
|
||||||
num_repos: 14
|
num_repos: 15
|
||||||
num_teams: 0
|
num_teams: 0
|
||||||
num_members: 0
|
num_members: 0
|
||||||
visibility: 0
|
visibility: 0
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/forgejo/semver"
|
"code.gitea.io/gitea/models/forgejo/semver"
|
||||||
forgejo_v1_20 "code.gitea.io/gitea/models/forgejo_migrations/v1_20"
|
forgejo_v1_20 "code.gitea.io/gitea/models/forgejo_migrations/v1_20"
|
||||||
|
forgejo_v1_22 "code.gitea.io/gitea/models/forgejo_migrations/v1_22"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -43,6 +44,10 @@ var migrations = []*Migration{
|
||||||
NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable),
|
NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable),
|
||||||
// v2 -> v3
|
// v2 -> v3
|
||||||
NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable),
|
NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable),
|
||||||
|
// v3 -> v4
|
||||||
|
NewMigration("Add default_permissions to repo_unit", forgejo_v1_22.AddDefaultPermissionsToRepoUnit),
|
||||||
|
// v4 -> v5
|
||||||
|
NewMigration("create the forgejo_repo_flag table", forgejo_v1_22.CreateRepoFlagTable),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||||
|
|
17
models/forgejo_migrations/v1_22/v4.go
Normal file
17
models/forgejo_migrations/v1_22/v4.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_22 //nolint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddDefaultPermissionsToRepoUnit(x *xorm.Engine) error {
|
||||||
|
type RepoUnit struct {
|
||||||
|
ID int64
|
||||||
|
DefaultPermissions int `xorm:"NOT NULL DEFAULT 0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync(&RepoUnit{})
|
||||||
|
}
|
22
models/forgejo_migrations/v1_22/v5.go
Normal file
22
models/forgejo_migrations/v1_22/v5.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_22 //nolint
|
||||||
|
|
||||||
|
import (
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RepoFlag struct {
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
|
||||||
|
Name string `xorm:"UNIQUE(s) INDEX"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RepoFlag) TableName() string {
|
||||||
|
return "forgejo_repo_flag"
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRepoFlagTable(x *xorm.Engine) error {
|
||||||
|
return x.Sync(new(RepoFlag))
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ import (
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/structs"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
@ -97,3 +98,29 @@ func TestMigrate_InsertIssueComments(t *testing.T) {
|
||||||
|
|
||||||
unittest.CheckConsistencyFor(t, &issues_model.Issue{})
|
unittest.CheckConsistencyFor(t, &issues_model.Issue{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateCommentsMigrationsByType(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||||
|
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1, IssueID: issue.ID})
|
||||||
|
|
||||||
|
// Set repository to migrated from Gitea.
|
||||||
|
repo.OriginalServiceType = structs.GiteaService
|
||||||
|
repo_model.UpdateRepositoryCols(db.DefaultContext, repo, "original_service_type")
|
||||||
|
|
||||||
|
// Set comment to have an original author.
|
||||||
|
comment.OriginalAuthor = "Example User"
|
||||||
|
comment.OriginalAuthorID = 1
|
||||||
|
comment.PosterID = 0
|
||||||
|
_, err := db.GetEngine(db.DefaultContext).ID(comment.ID).Cols("original_author", "original_author_id", "poster_id").Update(comment)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NoError(t, issues_model.UpdateCommentsMigrationsByType(db.DefaultContext, structs.GiteaService, "1", 513))
|
||||||
|
|
||||||
|
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1, IssueID: issue.ID})
|
||||||
|
assert.Empty(t, comment.OriginalAuthor)
|
||||||
|
assert.Empty(t, comment.OriginalAuthorID)
|
||||||
|
assert.EqualValues(t, 513, comment.PosterID)
|
||||||
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
package base
|
package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
"golang.org/x/crypto/pbkdf2"
|
"golang.org/x/crypto/pbkdf2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@
|
||||||
package v1_14 //nolint
|
package v1_14 //nolint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
"golang.org/x/crypto/argon2"
|
"golang.org/x/crypto/argon2"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"golang.org/x/crypto/pbkdf2"
|
"golang.org/x/crypto/pbkdf2"
|
||||||
|
|
|
@ -4,13 +4,7 @@
|
||||||
package v1_21 //nolint
|
package v1_21 //nolint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
giturl "code.gitea.io/gitea/modules/git/url"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
|
@ -73,7 +67,7 @@ func migratePullMirrors(x *xorm.Engine) error {
|
||||||
start += len(mirrors)
|
start += len(mirrors)
|
||||||
|
|
||||||
for _, m := range mirrors {
|
for _, m := range mirrors {
|
||||||
remoteAddress, err := getRemoteAddress(m.RepoOwner, m.RepoName, "origin")
|
remoteAddress, err := repo_model.GetPushMirrorRemoteAddress(m.RepoOwner, m.RepoName, "origin")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -136,7 +130,7 @@ func migratePushMirrors(x *xorm.Engine) error {
|
||||||
start += len(mirrors)
|
start += len(mirrors)
|
||||||
|
|
||||||
for _, m := range mirrors {
|
for _, m := range mirrors {
|
||||||
remoteAddress, err := getRemoteAddress(m.RepoOwner, m.RepoName, m.RemoteName)
|
remoteAddress, err := repo_model.GetPushMirrorRemoteAddress(m.RepoOwner, m.RepoName, m.RemoteName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -160,20 +154,3 @@ func migratePushMirrors(x *xorm.Engine) error {
|
||||||
|
|
||||||
return sess.Commit()
|
return sess.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
|
|
||||||
repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git")
|
|
||||||
|
|
||||||
remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
u, err := giturl.Parse(remoteURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
u.User = nil
|
|
||||||
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -33,6 +33,16 @@ func (p *Permission) IsAdmin() bool {
|
||||||
return p.AccessMode >= perm_model.AccessModeAdmin
|
return p.AccessMode >= perm_model.AccessModeAdmin
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsGloballyWriteable returns true if the unit is writeable by all users of the instance.
|
||||||
|
func (p *Permission) IsGloballyWriteable(unitType unit.Type) bool {
|
||||||
|
for _, u := range p.Units {
|
||||||
|
if u.Type == unitType {
|
||||||
|
return u.DefaultPermissions == repo_model.UnitAccessModeWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// HasAccess returns true if the current user has at least read access to any unit of this repository
|
// HasAccess returns true if the current user has at least read access to any unit of this repository
|
||||||
func (p *Permission) HasAccess() bool {
|
func (p *Permission) HasAccess() bool {
|
||||||
if p.UnitsMode == nil {
|
if p.UnitsMode == nil {
|
||||||
|
@ -198,7 +208,19 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
||||||
if err := repo.LoadOwner(ctx); err != nil {
|
if err := repo.LoadOwner(ctx); err != nil {
|
||||||
return perm, err
|
return perm, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !repo.Owner.IsOrganization() {
|
if !repo.Owner.IsOrganization() {
|
||||||
|
// for a public repo, different repo units may have different default
|
||||||
|
// permissions for non-restricted users.
|
||||||
|
if !repo.IsPrivate && !user.IsRestricted && len(repo.Units) > 0 {
|
||||||
|
perm.UnitsMode = make(map[unit.Type]perm_model.AccessMode)
|
||||||
|
for _, u := range repo.Units {
|
||||||
|
if _, ok := perm.UnitsMode[u.Type]; !ok {
|
||||||
|
perm.UnitsMode[u.Type] = u.DefaultPermissions.ToAccessMode(perm.AccessMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return perm, nil
|
return perm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,10 +261,12 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// for a public repo on an organization, a non-restricted user has read permission on non-team defined units.
|
// for a public repo on an organization, a non-restricted user should
|
||||||
|
// have the same permission on non-team defined units as the default
|
||||||
|
// permissions for the repo unit.
|
||||||
if !found && !repo.IsPrivate && !user.IsRestricted {
|
if !found && !repo.IsPrivate && !user.IsRestricted {
|
||||||
if _, ok := perm.UnitsMode[u.Type]; !ok {
|
if _, ok := perm.UnitsMode[u.Type]; !ok {
|
||||||
perm.UnitsMode[u.Type] = perm_model.AccessModeRead
|
perm.UnitsMode[u.Type] = u.DefaultPermissions.ToAccessMode(perm_model.AccessModeRead)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,7 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
|
||||||
return false, nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
|
doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil, err
|
return false, nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,16 @@ package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
giturl "code.gitea.io/gitea/modules/git/url"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
@ -129,3 +135,21 @@ func PushMirrorsIterate(ctx context.Context, limit int, f func(idx int, bean any
|
||||||
}
|
}
|
||||||
return sess.Iterate(new(PushMirror), f)
|
return sess.Iterate(new(PushMirror), f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPushMirrorRemoteAddress returns the address of associated with a repository's given remote.
|
||||||
|
func GetPushMirrorRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
|
||||||
|
repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git")
|
||||||
|
|
||||||
|
remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := giturl.Parse(remoteURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
u.User = nil
|
||||||
|
|
||||||
|
return u.String(), nil
|
||||||
|
}
|
||||||
|
|
102
models/repo/repo_flags.go
Normal file
102
models/repo/repo_flags.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RepoFlag represents a single flag against a repository
|
||||||
|
type RepoFlag struct { //revive:disable-line:exported
|
||||||
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
|
||||||
|
Name string `xorm:"UNIQUE(s) INDEX"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
db.RegisterModel(new(RepoFlag))
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName provides the real table name
|
||||||
|
func (RepoFlag) TableName() string {
|
||||||
|
return "forgejo_repo_flag"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFlags returns the array of flags on the repo.
|
||||||
|
func (repo *Repository) ListFlags(ctx context.Context) ([]RepoFlag, error) {
|
||||||
|
var flags []RepoFlag
|
||||||
|
err := db.GetEngine(ctx).Table(&RepoFlag{}).Where("repo_id = ?", repo.ID).Find(&flags)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return flags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFlagged returns whether a repo has any flags or not
|
||||||
|
func (repo *Repository) IsFlagged(ctx context.Context) bool {
|
||||||
|
has, _ := db.Exist[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID})
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFlag returns a single RepoFlag based on its name
|
||||||
|
func (repo *Repository) GetFlag(ctx context.Context, flagName string) (bool, *RepoFlag, error) {
|
||||||
|
flag, has, err := db.Get[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID, "name": flagName})
|
||||||
|
if err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
return has, flag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasFlag returns true if a repo has a given flag, false otherwise
|
||||||
|
func (repo *Repository) HasFlag(ctx context.Context, flagName string) bool {
|
||||||
|
has, _ := db.Exist[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID, "name": flagName})
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFlag adds a new flag to the repo
|
||||||
|
func (repo *Repository) AddFlag(ctx context.Context, flagName string) error {
|
||||||
|
return db.Insert(ctx, RepoFlag{
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Name: flagName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteFlag removes a flag from the repo
|
||||||
|
func (repo *Repository) DeleteFlag(ctx context.Context, flagName string) (int64, error) {
|
||||||
|
return db.DeleteByBean(ctx, &RepoFlag{RepoID: repo.ID, Name: flagName})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceAllFlags replaces all flags of a repo with a new set
|
||||||
|
func (repo *Repository) ReplaceAllFlags(ctx context.Context, flagNames []string) error {
|
||||||
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer committer.Close()
|
||||||
|
|
||||||
|
if err := db.DeleteBeans(ctx, &RepoFlag{RepoID: repo.ID}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(flagNames) == 0 {
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
var flags []RepoFlag
|
||||||
|
for _, name := range flagNames {
|
||||||
|
flags = append(flags, RepoFlag{
|
||||||
|
RepoID: repo.ID,
|
||||||
|
Name: name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := db.Insert(ctx, &flags); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return committer.Commit()
|
||||||
|
}
|
114
models/repo/repo_flags_test.go
Normal file
114
models/repo/repo_flags_test.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRepositoryFlags(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||||
|
|
||||||
|
// ********************
|
||||||
|
// ** NEGATIVE TESTS **
|
||||||
|
// ********************
|
||||||
|
|
||||||
|
// Unless we add flags, the repo has none
|
||||||
|
flags, err := repo.ListFlags(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, flags)
|
||||||
|
|
||||||
|
// If the repo has no flags, it is not flagged
|
||||||
|
flagged := repo.IsFlagged(db.DefaultContext)
|
||||||
|
assert.False(t, flagged)
|
||||||
|
|
||||||
|
// Trying to find a flag when there is none
|
||||||
|
has := repo.HasFlag(db.DefaultContext, "foo")
|
||||||
|
assert.False(t, has)
|
||||||
|
|
||||||
|
// Trying to retrieve a non-existent flag indicates not found
|
||||||
|
has, _, err = repo.GetFlag(db.DefaultContext, "foo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.False(t, has)
|
||||||
|
|
||||||
|
// Deleting a non-existent flag fails
|
||||||
|
deleted, err := repo.DeleteFlag(db.DefaultContext, "no-such-flag")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(0), deleted)
|
||||||
|
|
||||||
|
// ********************
|
||||||
|
// ** POSITIVE TESTS **
|
||||||
|
// ********************
|
||||||
|
|
||||||
|
// Adding a flag works
|
||||||
|
err = repo.AddFlag(db.DefaultContext, "foo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Adding it again fails
|
||||||
|
err = repo.AddFlag(db.DefaultContext, "foo")
|
||||||
|
assert.Error(t, err)
|
||||||
|
|
||||||
|
// Listing flags includes the one we added
|
||||||
|
flags, err = repo.ListFlags(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, flags, 1)
|
||||||
|
assert.Equal(t, "foo", flags[0].Name)
|
||||||
|
|
||||||
|
// With a flag added, the repo is flagged
|
||||||
|
flagged = repo.IsFlagged(db.DefaultContext)
|
||||||
|
assert.True(t, flagged)
|
||||||
|
|
||||||
|
// The flag can be found
|
||||||
|
has = repo.HasFlag(db.DefaultContext, "foo")
|
||||||
|
assert.True(t, has)
|
||||||
|
|
||||||
|
// Added flag can be retrieved
|
||||||
|
_, flag, err := repo.GetFlag(db.DefaultContext, "foo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "foo", flag.Name)
|
||||||
|
|
||||||
|
// Deleting a flag works
|
||||||
|
deleted, err = repo.DeleteFlag(db.DefaultContext, "foo")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), deleted)
|
||||||
|
|
||||||
|
// The list is now empty
|
||||||
|
flags, err = repo.ListFlags(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Empty(t, flags)
|
||||||
|
|
||||||
|
// Replacing an empty list works
|
||||||
|
err = repo.ReplaceAllFlags(db.DefaultContext, []string{"bar"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// The repo is now flagged with "bar"
|
||||||
|
has = repo.HasFlag(db.DefaultContext, "bar")
|
||||||
|
assert.True(t, has)
|
||||||
|
|
||||||
|
// Replacing a tag set with another works
|
||||||
|
err = repo.ReplaceAllFlags(db.DefaultContext, []string{"baz", "quux"})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// The repo now has two tags
|
||||||
|
flags, err = repo.ListFlags(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, flags, 2)
|
||||||
|
assert.Equal(t, "baz", flags[0].Name)
|
||||||
|
assert.Equal(t, "quux", flags[1].Name)
|
||||||
|
|
||||||
|
// Replacing flags with an empty set deletes all flags
|
||||||
|
err = repo.ReplaceAllFlags(db.DefaultContext, []string{})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// The repo is now unflagged
|
||||||
|
flagged = repo.IsFlagged(db.DefaultContext)
|
||||||
|
assert.False(t, flagged)
|
||||||
|
}
|
|
@ -138,12 +138,12 @@ func getTestCases() []struct {
|
||||||
{
|
{
|
||||||
name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
|
name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
|
||||||
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse},
|
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: util.OptionalBoolFalse},
|
||||||
count: 31,
|
count: 32,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
|
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative",
|
||||||
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse},
|
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: util.OptionalBoolFalse},
|
||||||
count: 36,
|
count: 37,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
|
name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName",
|
||||||
|
@ -158,7 +158,7 @@ func getTestCases() []struct {
|
||||||
{
|
{
|
||||||
name: "AllPublic/PublicRepositoriesOfOrganization",
|
name: "AllPublic/PublicRepositoriesOfOrganization",
|
||||||
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse},
|
opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: util.OptionalBoolFalse, Template: util.OptionalBoolFalse},
|
||||||
count: 31,
|
count: 32,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "AllTemplates",
|
name: "AllTemplates",
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
"code.gitea.io/gitea/models/perm"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
@ -39,13 +40,43 @@ func (err ErrUnitTypeNotExist) Unwrap() error {
|
||||||
return util.ErrNotExist
|
return util.ErrNotExist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RepoUnitAccessMode specifies the users access mode to a repo unit
|
||||||
|
type UnitAccessMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// UnitAccessModeUnset - no unit mode set
|
||||||
|
UnitAccessModeUnset UnitAccessMode = iota // 0
|
||||||
|
// UnitAccessModeNone no access
|
||||||
|
UnitAccessModeNone // 1
|
||||||
|
// UnitAccessModeRead read access
|
||||||
|
UnitAccessModeRead // 2
|
||||||
|
// UnitAccessModeWrite write access
|
||||||
|
UnitAccessModeWrite // 3
|
||||||
|
)
|
||||||
|
|
||||||
|
func (mode UnitAccessMode) ToAccessMode(modeIfUnset perm.AccessMode) perm.AccessMode {
|
||||||
|
switch mode {
|
||||||
|
case UnitAccessModeUnset:
|
||||||
|
return modeIfUnset
|
||||||
|
case UnitAccessModeNone:
|
||||||
|
return perm.AccessModeNone
|
||||||
|
case UnitAccessModeRead:
|
||||||
|
return perm.AccessModeRead
|
||||||
|
case UnitAccessModeWrite:
|
||||||
|
return perm.AccessModeWrite
|
||||||
|
default:
|
||||||
|
return perm.AccessModeNone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RepoUnit describes all units of a repository
|
// RepoUnit describes all units of a repository
|
||||||
type RepoUnit struct { //revive:disable-line:exported
|
type RepoUnit struct { //revive:disable-line:exported
|
||||||
ID int64
|
ID int64
|
||||||
RepoID int64 `xorm:"INDEX(s)"`
|
RepoID int64 `xorm:"INDEX(s)"`
|
||||||
Type unit.Type `xorm:"INDEX(s)"`
|
Type unit.Type `xorm:"INDEX(s)"`
|
||||||
Config convert.Conversion `xorm:"TEXT"`
|
Config convert.Conversion `xorm:"TEXT"`
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||||
|
DefaultPermissions UnitAccessMode `xorm:"NOT NULL DEFAULT 0"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -6,6 +6,8 @@ package repo
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/perm"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,3 +30,10 @@ func TestActionsConfig(t *testing.T) {
|
||||||
cfg.DisableWorkflow("test3.yaml")
|
cfg.DisableWorkflow("test3.yaml")
|
||||||
assert.EqualValues(t, "test1.yaml,test2.yaml,test3.yaml", cfg.ToString())
|
assert.EqualValues(t, "test1.yaml,test2.yaml,test3.yaml", cfg.ToString())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRepoUnitAccessMode(t *testing.T) {
|
||||||
|
assert.Equal(t, UnitAccessModeNone.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeNone)
|
||||||
|
assert.Equal(t, UnitAccessModeRead.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeRead)
|
||||||
|
assert.Equal(t, UnitAccessModeWrite.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeWrite)
|
||||||
|
assert.Equal(t, UnitAccessModeUnset.ToAccessMode(perm.AccessModeRead), perm.AccessModeRead)
|
||||||
|
}
|
||||||
|
|
|
@ -199,7 +199,7 @@ func FindTopics(ctx context.Context, opts *FindTopicOptions) ([]*Topic, int64, e
|
||||||
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
|
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
|
||||||
orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result
|
orderBy = "topic.name" // when render topics for a repo, it's better to sort them by name, to get consistent result
|
||||||
}
|
}
|
||||||
if opts.PageSize != 0 && opts.Page != 0 {
|
if opts.PageSize > 0 {
|
||||||
sess = db.SetSessionPagination(sess, opts)
|
sess = db.SetSessionPagination(sess, opts)
|
||||||
}
|
}
|
||||||
topics := make([]*Topic, 0, 10)
|
topics := make([]*Topic, 0, 10)
|
||||||
|
|
113
models/unittest/mock_http.go
Normal file
113
models/unittest/mock_http.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// Copyright 2017 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package unittest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mocks HTTP responses of a third-party service (such as GitHub, GitLab…)
|
||||||
|
// This has two modes:
|
||||||
|
// - live mode: the requests made to the mock HTTP server are transmitted to the live
|
||||||
|
// service, and responses are saved as test data files
|
||||||
|
// - test mode: the responses to requests to the mock HTTP server are read from the
|
||||||
|
// test data files
|
||||||
|
func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveMode bool) *httptest.Server {
|
||||||
|
mockServerBaseURL := ""
|
||||||
|
ignoredHeaders := []string{"cf-ray", "server", "date", "report-to", "nel", "x-request-id"}
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
path := NormalizedFullPath(r.URL)
|
||||||
|
log.Info("Mock HTTP Server: got request for path %s", r.URL.Path)
|
||||||
|
// TODO check request method (support POST?)
|
||||||
|
fixturePath := fmt.Sprintf("%s/%s", testDataDir, strings.ReplaceAll(path, "/", "_"))
|
||||||
|
if liveMode {
|
||||||
|
liveURL := fmt.Sprintf("%s%s", liveServerBaseURL, path)
|
||||||
|
|
||||||
|
request, err := http.NewRequest(r.Method, liveURL, nil)
|
||||||
|
assert.NoError(t, err, "constructing an HTTP request to %s failed", liveURL)
|
||||||
|
for headerName, headerValues := range r.Header {
|
||||||
|
// do not pass on the encoding: let the Transport of the HTTP client handle that for us
|
||||||
|
if strings.ToLower(headerName) != "accept-encoding" {
|
||||||
|
for _, headerValue := range headerValues {
|
||||||
|
request.Header.Add(headerName, headerValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := http.DefaultClient.Do(request)
|
||||||
|
assert.NoError(t, err, "HTTP request to %s failed: %s", liveURL)
|
||||||
|
|
||||||
|
fixture, err := os.Create(fixturePath)
|
||||||
|
assert.NoError(t, err, "failed to open the fixture file %s for writing", fixturePath)
|
||||||
|
defer fixture.Close()
|
||||||
|
fixtureWriter := bufio.NewWriter(fixture)
|
||||||
|
|
||||||
|
for headerName, headerValues := range response.Header {
|
||||||
|
for _, headerValue := range headerValues {
|
||||||
|
if !slices.Contains(ignoredHeaders, strings.ToLower(headerName)) {
|
||||||
|
_, err := fixtureWriter.WriteString(fmt.Sprintf("%s: %s\n", headerName, headerValue))
|
||||||
|
assert.NoError(t, err, "writing the header of the HTTP response to the fixture file failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err = fixtureWriter.WriteString("\n")
|
||||||
|
assert.NoError(t, err, "writing the header of the HTTP response to the fixture file failed")
|
||||||
|
fixtureWriter.Flush()
|
||||||
|
|
||||||
|
log.Info("Mock HTTP Server: writing response to %s", fixturePath)
|
||||||
|
_, err = io.Copy(fixture, response.Body)
|
||||||
|
assert.NoError(t, err, "writing the body of the HTTP response to %s failed", liveURL)
|
||||||
|
|
||||||
|
err = fixture.Sync()
|
||||||
|
assert.NoError(t, err, "writing the body of the HTTP response to the fixture file failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
fixture, err := os.ReadFile(fixturePath)
|
||||||
|
assert.NoError(t, err, "missing mock HTTP response: "+fixturePath)
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
|
// replace any mention of the live HTTP service by the mocked host
|
||||||
|
stringFixture := strings.ReplaceAll(string(fixture), liveServerBaseURL, mockServerBaseURL)
|
||||||
|
// parse back the fixture file into a series of HTTP headers followed by response body
|
||||||
|
lines := strings.Split(stringFixture, "\n")
|
||||||
|
for idx, line := range lines {
|
||||||
|
colonIndex := strings.Index(line, ": ")
|
||||||
|
if colonIndex != -1 {
|
||||||
|
w.Header().Set(line[0:colonIndex], line[colonIndex+2:])
|
||||||
|
} else {
|
||||||
|
// we reached the end of the headers (empty line), so what follows is the body
|
||||||
|
responseBody := strings.Join(lines[idx+1:], "\n")
|
||||||
|
_, err := w.Write([]byte(responseBody))
|
||||||
|
assert.NoError(t, err, "writing the body of the HTTP response failed")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
mockServerBaseURL = server.URL
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizedFullPath(url *url.URL) string {
|
||||||
|
// TODO normalize path (remove trailing slash?)
|
||||||
|
// TODO normalize RawQuery (order query parameters?)
|
||||||
|
if len(url.Query()) == 0 {
|
||||||
|
return url.EscapedPath()
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s?%s", url.EscapedPath(), url.RawQuery)
|
||||||
|
}
|
|
@ -189,6 +189,25 @@ func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error)
|
||||||
return emails, nil
|
return emails, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActivatedEmailAddress struct {
|
||||||
|
ID int64
|
||||||
|
Email string
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetActivatedEmailAddresses(ctx context.Context, uid int64) ([]*ActivatedEmailAddress, error) {
|
||||||
|
emails := make([]*ActivatedEmailAddress, 0, 8)
|
||||||
|
if err := db.GetEngine(ctx).
|
||||||
|
Table("email_address").
|
||||||
|
Select("id, email").
|
||||||
|
Where("uid=?", uid).
|
||||||
|
And("is_activated=?", true).
|
||||||
|
Asc("id").
|
||||||
|
Find(&emails); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return emails, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetEmailAddressByID gets a user's email address by ID
|
// GetEmailAddressByID gets a user's email address by ID
|
||||||
func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, error) {
|
func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, error) {
|
||||||
// User ID is required for security reasons
|
// User ID is required for security reasons
|
||||||
|
@ -356,31 +375,7 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e
|
||||||
return UpdateUserCols(ctx, user, "rands")
|
return UpdateUserCols(ctx, user, "rands")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeEmailPrimary sets primary email address of given user.
|
func makeEmailPrimary(ctx context.Context, user *User, email *EmailAddress) error {
|
||||||
func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
|
|
||||||
has, err := db.GetEngine(ctx).Get(email)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if !has {
|
|
||||||
return ErrEmailAddressNotExist{Email: email.Email}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !email.IsActivated {
|
|
||||||
return ErrEmailNotActivated
|
|
||||||
}
|
|
||||||
|
|
||||||
user := &User{}
|
|
||||||
has, err = db.GetEngine(ctx).ID(email.UID).Get(user)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
} else if !has {
|
|
||||||
return ErrUserNotExist{
|
|
||||||
UID: email.UID,
|
|
||||||
Name: "",
|
|
||||||
KeyID: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -410,6 +405,57 @@ func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
|
||||||
return committer.Commit()
|
return committer.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReplaceInactivePrimaryEmail replaces the primary email of a given user, even if the primary is not yet activated.
|
||||||
|
func ReplaceInactivePrimaryEmail(ctx context.Context, oldEmail string, email *EmailAddress) error {
|
||||||
|
user := &User{}
|
||||||
|
has, err := db.GetEngine(ctx).ID(email.UID).Get(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !has {
|
||||||
|
return ErrUserNotExist{
|
||||||
|
UID: email.UID,
|
||||||
|
Name: "",
|
||||||
|
KeyID: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = AddEmailAddress(ctx, email)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = makeEmailPrimary(ctx, user, email)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeleteEmailAddress(ctx, &EmailAddress{UID: email.UID, Email: oldEmail})
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeEmailPrimary sets primary email address of given user.
|
||||||
|
func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
|
||||||
|
has, err := db.GetEngine(ctx).Get(email)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !has {
|
||||||
|
return ErrEmailAddressNotExist{Email: email.Email}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !email.IsActivated {
|
||||||
|
return ErrEmailNotActivated
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &User{}
|
||||||
|
has, err = db.GetEngine(ctx).ID(email.UID).Get(user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if !has {
|
||||||
|
return ErrUserNotExist{UID: email.UID}
|
||||||
|
}
|
||||||
|
|
||||||
|
return makeEmailPrimary(ctx, user, email)
|
||||||
|
}
|
||||||
|
|
||||||
// VerifyActiveEmailCode verifies active email code when active account
|
// VerifyActiveEmailCode verifies active email code when active account
|
||||||
func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
|
func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
|
||||||
minutes := setting.Service.ActiveCodeLives
|
minutes := setting.Service.ActiveCodeLives
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package user_test
|
package user_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
@ -166,6 +167,28 @@ func TestMakeEmailPrimary(t *testing.T) {
|
||||||
assert.Equal(t, "user101@example.com", user.Email)
|
assert.Equal(t, "user101@example.com", user.Email)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReplaceInactivePrimaryEmail(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
email := &user_model.EmailAddress{
|
||||||
|
Email: "user9999999@example.com",
|
||||||
|
UID: 9999999,
|
||||||
|
}
|
||||||
|
err := user_model.ReplaceInactivePrimaryEmail(db.DefaultContext, "user10@example.com", email)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, user_model.IsErrUserNotExist(err))
|
||||||
|
|
||||||
|
email = &user_model.EmailAddress{
|
||||||
|
Email: "user201@example.com",
|
||||||
|
UID: 10,
|
||||||
|
}
|
||||||
|
err = user_model.ReplaceInactivePrimaryEmail(db.DefaultContext, "user10@example.com", email)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
|
||||||
|
assert.Equal(t, "user201@example.com", user.Email)
|
||||||
|
}
|
||||||
|
|
||||||
func TestActivate(t *testing.T) {
|
func TestActivate(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
@ -309,3 +332,37 @@ func TestEmailAddressValidate(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetActivatedEmailAddresses(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
UID int64
|
||||||
|
expected []*user_model.ActivatedEmailAddress
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
UID: 1,
|
||||||
|
expected: []*user_model.ActivatedEmailAddress{{ID: 9, Email: "user1@example.com"}, {ID: 33, Email: "user1-2@example.com"}, {ID: 34, Email: "user1-3@example.com"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: 2,
|
||||||
|
expected: []*user_model.ActivatedEmailAddress{{ID: 3, Email: "user2@example.com"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: 4,
|
||||||
|
expected: []*user_model.ActivatedEmailAddress{{ID: 11, Email: "user4@example.com"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
UID: 11,
|
||||||
|
expected: []*user_model.ActivatedEmailAddress{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(fmt.Sprintf("User %d", testCase.UID), func(t *testing.T) {
|
||||||
|
emails, err := user_model.GetActivatedEmailAddresses(db.DefaultContext, testCase.UID)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, testCase.expected, emails)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -228,6 +228,12 @@ func GetAllUsers(ctx context.Context) ([]*User, error) {
|
||||||
return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).Find(&users)
|
return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).Find(&users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetAllAdmins returns a slice of all adminusers found in DB.
|
||||||
|
func GetAllAdmins(ctx context.Context) ([]*User, error) {
|
||||||
|
users := make([]*User, 0)
|
||||||
|
return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).And("is_admin = ?", true).Find(&users)
|
||||||
|
}
|
||||||
|
|
||||||
// IsLocal returns true if user login type is LoginPlain.
|
// IsLocal returns true if user login type is LoginPlain.
|
||||||
func (u *User) IsLocal() bool {
|
func (u *User) IsLocal() bool {
|
||||||
return u.LoginType <= auth.Plain
|
return u.LoginType <= auth.Plain
|
||||||
|
|
|
@ -533,6 +533,16 @@ func TestIsUserVisibleToViewer(t *testing.T) {
|
||||||
test(user31, nil, false)
|
test(user31, nil, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetAllAdmins(t *testing.T) {
|
||||||
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
|
admins, err := user_model.GetAllAdmins(db.DefaultContext)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Len(t, admins, 1)
|
||||||
|
assert.Equal(t, int64(1), admins[0].ID)
|
||||||
|
}
|
||||||
|
|
||||||
func Test_ValidateUser(t *testing.T) {
|
func Test_ValidateUser(t *testing.T) {
|
||||||
oldSetting := setting.Service.AllowedUserVisibilityModesSlice
|
oldSetting := setting.Service.AllowedUserVisibilityModesSlice
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -552,6 +562,11 @@ func Test_ValidateUser(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_NormalizeUserFromEmail(t *testing.T) {
|
func Test_NormalizeUserFromEmail(t *testing.T) {
|
||||||
|
oldSetting := setting.Service.AllowDotsInUsernames
|
||||||
|
defer func() {
|
||||||
|
setting.Service.AllowDotsInUsernames = oldSetting
|
||||||
|
}()
|
||||||
|
setting.Service.AllowDotsInUsernames = true
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
Input string
|
Input string
|
||||||
Expected string
|
Expected string
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
package hash
|
package hash
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
"golang.org/x/crypto/pbkdf2"
|
"golang.org/x/crypto/pbkdf2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,9 @@
|
||||||
package avatar
|
package avatar
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HashAvatar will generate a unique string, which ensures that when there's a
|
// HashAvatar will generate a unique string, which ensures that when there's a
|
||||||
|
|
|
@ -7,11 +7,10 @@
|
||||||
package identicon
|
package identicon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const minImageSize = 16
|
const minImageSize = 16
|
||||||
|
|
|
@ -5,6 +5,7 @@ package base
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -22,7 +23,6 @@ import (
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// EncodeSha1 string to sha1 hex value.
|
// EncodeSha1 string to sha1 hex value.
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
@ -38,6 +39,7 @@ type APIContext struct {
|
||||||
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
|
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
|
||||||
|
|
||||||
Repo *Repository
|
Repo *Repository
|
||||||
|
Comment *issues_model.Comment
|
||||||
Org *APIOrganization
|
Org *APIOrganization
|
||||||
Package *Package
|
Package *Package
|
||||||
}
|
}
|
||||||
|
|
91
modules/doctor/push_mirror_consistency.go
Normal file
91
modules/doctor/push_mirror_consistency.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
// Copyright 2023 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package doctor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FixPushMirrorsWithoutGitRemote(ctx context.Context, logger log.Logger, autofix bool) error {
|
||||||
|
var missingMirrors []*repo_model.PushMirror
|
||||||
|
|
||||||
|
err := db.Iterate(ctx, builder.Gt{"id": 0}, func(ctx context.Context, repo *repo_model.Repository) error {
|
||||||
|
pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, db.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(pushMirrors); i++ {
|
||||||
|
_, err = repo_model.GetPushMirrorRemoteAddress(repo.OwnerName, repo.Name, pushMirrors[i].RemoteName)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "No such remote") {
|
||||||
|
missingMirrors = append(missingMirrors, pushMirrors[i])
|
||||||
|
} else if logger != nil {
|
||||||
|
logger.Warn("Unable to retrieve the remote address of a mirror: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Critical("Unable to iterate across repounits to fix push mirrors without a git remote: Error %v", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
count := len(missingMirrors)
|
||||||
|
if !autofix {
|
||||||
|
if logger != nil {
|
||||||
|
if count == 0 {
|
||||||
|
logger.Info("Found no push mirrors with missing git remotes")
|
||||||
|
} else {
|
||||||
|
logger.Warn("Found %d push mirrors with missing git remotes", count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(missingMirrors); i++ {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Info("Removing push mirror #%d (remote: %s), for repo: %s/%s",
|
||||||
|
missingMirrors[i].ID,
|
||||||
|
missingMirrors[i].RemoteName,
|
||||||
|
missingMirrors[i].GetRepository(ctx).OwnerName,
|
||||||
|
missingMirrors[i].GetRepository(ctx).Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{
|
||||||
|
ID: missingMirrors[i].ID,
|
||||||
|
RepoID: missingMirrors[i].RepoID,
|
||||||
|
RemoteName: missingMirrors[i].RemoteName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if logger != nil {
|
||||||
|
logger.Critical("Error removing a push mirror (repo_id: %d, push_mirror: %d): %s", missingMirrors[i].Repo.ID, missingMirrors[i].ID, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
Register(&Check{
|
||||||
|
Title: "Check for push mirrors without a git remote configured",
|
||||||
|
Name: "fix-push-mirrors-without-git-remote",
|
||||||
|
IsDefault: false,
|
||||||
|
Run: FixPushMirrorsWithoutGitRemote,
|
||||||
|
Priority: 7,
|
||||||
|
})
|
||||||
|
}
|
|
@ -515,6 +515,62 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi
|
||||||
return fileStatus, nil
|
return fileStatus, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseCommitRenames(renames *[][2]string, stdout io.Reader) {
|
||||||
|
rd := bufio.NewReader(stdout)
|
||||||
|
for {
|
||||||
|
// Skip (R || three digits || NULL byte)
|
||||||
|
_, err := rd.Discard(5)
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oldFileName, err := rd.ReadString('\x00')
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newFileName, err := rd.ReadString('\x00')
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
log.Error("Unexpected error whilst reading from git log --name-status. Error: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
oldFileName = strings.TrimSuffix(oldFileName, "\x00")
|
||||||
|
newFileName = strings.TrimSuffix(newFileName, "\x00")
|
||||||
|
*renames = append(*renames, [2]string{oldFileName, newFileName})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommitFileRenames returns the renames that the commit contains.
|
||||||
|
func GetCommitFileRenames(ctx context.Context, repoPath, commitID string) ([][2]string, error) {
|
||||||
|
renames := [][2]string{}
|
||||||
|
stdout, w := io.Pipe()
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
parseCommitRenames(&renames, stdout)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
stderr := new(bytes.Buffer)
|
||||||
|
err := NewCommand(ctx, "show", "--name-status", "--pretty=format:", "-z", "--diff-filter=R").AddDynamicArguments(commitID).Run(&RunOpts{
|
||||||
|
Dir: repoPath,
|
||||||
|
Stdout: w,
|
||||||
|
Stderr: stderr,
|
||||||
|
})
|
||||||
|
w.Close() // Close writer to exit parsing goroutine
|
||||||
|
if err != nil {
|
||||||
|
return nil, ConcatenateError(err, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
<-done
|
||||||
|
return renames, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository.
|
// GetFullCommitID returns full length (40) of commit ID by given short SHA in a repository.
|
||||||
func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) {
|
func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) {
|
||||||
commitID, _, err := NewCommand(ctx, "rev-parse").AddDynamicArguments(shortID).RunStdString(&RunOpts{Dir: repoPath})
|
commitID, _, err := NewCommand(ctx, "rev-parse").AddDynamicArguments(shortID).RunStdString(&RunOpts{Dir: repoPath})
|
||||||
|
|
|
@ -278,3 +278,30 @@ func TestGetCommitFileStatusMerges(t *testing.T) {
|
||||||
assert.Equal(t, commitFileStatus.Removed, expected.Removed)
|
assert.Equal(t, commitFileStatus.Removed, expected.Removed)
|
||||||
assert.Equal(t, commitFileStatus.Modified, expected.Modified)
|
assert.Equal(t, commitFileStatus.Modified, expected.Modified)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseCommitRenames(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
output string
|
||||||
|
renames [][2]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
output: "R090\x00renamed.txt\x00history.txt\x00",
|
||||||
|
renames: [][2]string{{"renamed.txt", "history.txt"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
output: "R090\x00renamed.txt\x00history.txt\x00R000\x00corruptedstdouthere",
|
||||||
|
renames: [][2]string{{"renamed.txt", "history.txt"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
output: "R100\x00renamed.txt\x00history.txt\x00R001\x00readme.md\x00README.md\x00",
|
||||||
|
renames: [][2]string{{"renamed.txt", "history.txt"}, {"readme.md", "README.md"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testcase := range testcases {
|
||||||
|
renames := [][2]string{}
|
||||||
|
parseCommitRenames(&renames, strings.NewReader(testcase.output))
|
||||||
|
|
||||||
|
assert.Equal(t, testcase.renames, renames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,12 +4,11 @@
|
||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cache represents a caching interface
|
// Cache represents a caching interface
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
package git
|
package git
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
// GetBlobByPath get the blob object according the path
|
// GetBlobByPath get the blob object according the path
|
||||||
func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
|
func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
|
||||||
entry, err := t.GetTreeEntryByPath(relpath)
|
entry, err := t.GetTreeEntryByPath(relpath)
|
||||||
|
@ -17,3 +20,21 @@ func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
|
||||||
|
|
||||||
return nil, ErrNotExist{"", relpath}
|
return nil, ErrNotExist{"", relpath}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBlobByFoldedPath returns the blob object at relpath, regardless of the
|
||||||
|
// case of relpath. If there are multiple files with the same case-insensitive
|
||||||
|
// name, the first one found will be returned.
|
||||||
|
func (t *Tree) GetBlobByFoldedPath(relpath string) (*Blob, error) {
|
||||||
|
entries, err := t.ListEntries()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if strings.EqualFold(entry.Name(), relpath) {
|
||||||
|
return t.GetBlobByPath(entry.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotExist{"", relpath}
|
||||||
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package lfs
|
package lfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"hash"
|
"hash"
|
||||||
|
@ -12,8 +13,6 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/storage"
|
"code.gitea.io/gitea/modules/storage"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
package lfs
|
package lfs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -12,8 +13,6 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -29,12 +29,17 @@ func CleanValue(value []byte) []byte {
|
||||||
value = bytes.TrimSpace(value)
|
value = bytes.TrimSpace(value)
|
||||||
rs := bytes.Runes(value)
|
rs := bytes.Runes(value)
|
||||||
result := make([]rune, 0, len(rs))
|
result := make([]rune, 0, len(rs))
|
||||||
|
needsDash := false
|
||||||
for _, r := range rs {
|
for _, r := range rs {
|
||||||
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' || r == '-' {
|
switch {
|
||||||
|
case unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_':
|
||||||
|
if needsDash && len(result) > 0 {
|
||||||
|
result = append(result, '-')
|
||||||
|
}
|
||||||
|
needsDash = false
|
||||||
result = append(result, unicode.ToLower(r))
|
result = append(result, unicode.ToLower(r))
|
||||||
}
|
default:
|
||||||
if unicode.IsSpace(r) {
|
needsDash = true
|
||||||
result = append(result, '-')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []byte(string(result))
|
return []byte(string(result))
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
package common
|
package common
|
||||||
|
|
||||||
|
@ -15,44 +16,45 @@ func TestCleanValue(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
// Github behavior test cases
|
// Github behavior test cases
|
||||||
{"", ""},
|
{"", ""},
|
||||||
{"test(0)", "test0"},
|
{"test.0.1", "test-0-1"},
|
||||||
{"test!1", "test1"},
|
{"test(0)", "test-0"},
|
||||||
{"test:2", "test2"},
|
{"test!1", "test-1"},
|
||||||
{"test*3", "test3"},
|
{"test:2", "test-2"},
|
||||||
{"test!4", "test4"},
|
{"test*3", "test-3"},
|
||||||
{"test:5", "test5"},
|
{"test!4", "test-4"},
|
||||||
{"test*6", "test6"},
|
{"test:5", "test-5"},
|
||||||
{"test:6 a", "test6-a"},
|
{"test*6", "test-6"},
|
||||||
{"test:6 !b", "test6-b"},
|
{"test:6 a", "test-6-a"},
|
||||||
{"test:ad # df", "testad--df"},
|
{"test:6 !b", "test-6-b"},
|
||||||
{"test:ad #23 df 2*/*", "testad-23-df-2"},
|
{"test:ad # df", "test-ad-df"},
|
||||||
{"test:ad 23 df 2*/*", "testad-23-df-2"},
|
{"test:ad #23 df 2*/*", "test-ad-23-df-2"},
|
||||||
{"test:ad # 23 df 2*/*", "testad--23-df-2"},
|
{"test:ad 23 df 2*/*", "test-ad-23-df-2"},
|
||||||
|
{"test:ad # 23 df 2*/*", "test-ad-23-df-2"},
|
||||||
{"Anchors in Markdown", "anchors-in-markdown"},
|
{"Anchors in Markdown", "anchors-in-markdown"},
|
||||||
{"a_b_c", "a_b_c"},
|
{"a_b_c", "a_b_c"},
|
||||||
{"a-b-c", "a-b-c"},
|
{"a-b-c", "a-b-c"},
|
||||||
{"a-b-c----", "a-b-c----"},
|
{"a-b-c----", "a-b-c"},
|
||||||
{"test:6a", "test6a"},
|
{"test:6a", "test-6a"},
|
||||||
{"test:a6", "testa6"},
|
{"test:a6", "test-a6"},
|
||||||
{"tes a a a a", "tes-a-a---a--a"},
|
{"tes a a a a", "tes-a-a-a-a"},
|
||||||
{" tes a a a a ", "tes-a-a---a--a"},
|
{" tes a a a a ", "tes-a-a-a-a"},
|
||||||
{"Header with \"double quotes\"", "header-with-double-quotes"},
|
{"Header with \"double quotes\"", "header-with-double-quotes"},
|
||||||
{"Placeholder to force scrolling on link's click", "placeholder-to-force-scrolling-on-links-click"},
|
{"Placeholder to force scrolling on link's click", "placeholder-to-force-scrolling-on-link-s-click"},
|
||||||
{"tes()", "tes"},
|
{"tes()", "tes"},
|
||||||
{"tes(0)", "tes0"},
|
{"tes(0)", "tes-0"},
|
||||||
{"tes{0}", "tes0"},
|
{"tes{0}", "tes-0"},
|
||||||
{"tes[0]", "tes0"},
|
{"tes[0]", "tes-0"},
|
||||||
{"test【0】", "test0"},
|
{"test【0】", "test-0"},
|
||||||
{"tes…@a", "tesa"},
|
{"tes…@a", "tes-a"},
|
||||||
{"tes¥& a", "tes-a"},
|
{"tes¥& a", "tes-a"},
|
||||||
{"tes= a", "tes-a"},
|
{"tes= a", "tes-a"},
|
||||||
{"tes|a", "tesa"},
|
{"tes|a", "tes-a"},
|
||||||
{"tes\\a", "tesa"},
|
{"tes\\a", "tes-a"},
|
||||||
{"tes/a", "tesa"},
|
{"tes/a", "tes-a"},
|
||||||
{"a啊啊b", "a啊啊b"},
|
{"a啊啊b", "a啊啊b"},
|
||||||
{"c🤔️🤔️d", "cd"},
|
{"c🤔️🤔️d", "c-d"},
|
||||||
{"a⚡a", "aa"},
|
{"a⚡a", "a-a"},
|
||||||
{"e.~f", "ef"},
|
{"e.~f", "e-f"},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
assert.Equal(t, []byte(test.expect), CleanValue([]byte(test.param)), test.param)
|
assert.Equal(t, []byte(test.expect), CleanValue([]byte(test.param)), test.param)
|
||||||
|
|
|
@ -524,6 +524,18 @@ func TestMathBlock(t *testing.T) {
|
||||||
"$$a$$",
|
"$$a$$",
|
||||||
`<pre class="code-block is-loading"><code class="chroma language-math display">a</code></pre>` + nl,
|
`<pre class="code-block is-loading"><code class="chroma language-math display">a</code></pre>` + nl,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
`\[a b\]`,
|
||||||
|
`<pre class="code-block is-loading"><code class="chroma language-math display">a b</code></pre>` + nl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`\[a b]`,
|
||||||
|
`<p>[a b]</p>` + nl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`$$a`,
|
||||||
|
`<p>$$a</p>` + nl,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testcases {
|
for _, test := range testcases {
|
||||||
|
@ -534,6 +546,204 @@ func TestMathBlock(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFootnote(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
testcase string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
`Citation needed[^0].
|
||||||
|
[^0]: Source`,
|
||||||
|
`<p>Citation needed<sup id="fnref:user-content-0"><a href="#fn:user-content-0" rel="nofollow">1</a></sup>.</p>
|
||||||
|
<div>
|
||||||
|
<hr/>
|
||||||
|
<ol>
|
||||||
|
<li id="fn:user-content-0">
|
||||||
|
<p>Source <a href="#fnref:user-content-0" rel="nofollow">↩︎</a></p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^0]`,
|
||||||
|
`<p>Citation needed[^0]</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^1], Citation needed twice[^3]
|
||||||
|
[^3]: Source`,
|
||||||
|
`<p>Citation needed[^1], Citation needed twice<sup id="fnref:user-content-3"><a href="#fn:user-content-3" rel="nofollow">1</a></sup></p>
|
||||||
|
<div>
|
||||||
|
<hr/>
|
||||||
|
<ol>
|
||||||
|
<li id="fn:user-content-3">
|
||||||
|
<p>Source <a href="#fnref:user-content-3" rel="nofollow">↩︎</a></p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^0]
|
||||||
|
[^1]: Source`,
|
||||||
|
`<p>Citation needed[^0]</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^0]
|
||||||
|
[^0]: Source 1
|
||||||
|
[^0]: Source 2`,
|
||||||
|
`<p>Citation needed<sup id="fnref:user-content-0"><a href="#fn:user-content-0" rel="nofollow">1</a></sup></p>
|
||||||
|
<div>
|
||||||
|
<hr/>
|
||||||
|
<ol>
|
||||||
|
<li id="fn:user-content-0">
|
||||||
|
<p>Source 1 <a href="#fnref:user-content-0" rel="nofollow">↩︎</a></p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed![^0]
|
||||||
|
[^0]: Source`,
|
||||||
|
`<p>Citation needed<sup id="fnref:user-content-0"><a href="#fn:user-content-0" rel="nofollow">1</a></sup></p>
|
||||||
|
<div>
|
||||||
|
<hr/>
|
||||||
|
<ol>
|
||||||
|
<li id="fn:user-content-0">
|
||||||
|
<p>Source <a href="#fnref:user-content-0" rel="nofollow">↩︎</a></p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Trigger [^`,
|
||||||
|
`<p>Trigger [^</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Trigger 2 [^0`,
|
||||||
|
`<p>Trigger 2 [^0</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^0]
|
||||||
|
[^0]: Source with citation needed[^1]
|
||||||
|
[^1]: Source`,
|
||||||
|
`<p>Citation needed<sup id="fnref:user-content-0"><a href="#fn:user-content-0" rel="nofollow">1</a></sup></p>
|
||||||
|
<div>
|
||||||
|
<hr/>
|
||||||
|
<ol>
|
||||||
|
<li id="fn:user-content-0">
|
||||||
|
<p>Source with citation needed<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">2</a></sup> <a href="#fnref:user-content-0" rel="nofollow">↩︎</a></p>
|
||||||
|
</li>
|
||||||
|
<li id="fn:user-content-1">
|
||||||
|
<p>Source <a href="#fnref:user-content-1" rel="nofollow">↩︎</a></p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^#]
|
||||||
|
[^#]: Source`,
|
||||||
|
`<p>Citation needed<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1</a></sup></p>
|
||||||
|
<div>
|
||||||
|
<hr/>
|
||||||
|
<ol>
|
||||||
|
<li id="fn:user-content-1">
|
||||||
|
<p>Source <a href="#fnref:user-content-1" rel="nofollow">↩︎</a></p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^0]
|
||||||
|
[^0]: Source`,
|
||||||
|
`<p>Citation needed[^0]<br/>
|
||||||
|
[^0]: Source</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`[^0]: Source
|
||||||
|
|
||||||
|
Citation needed[^0].`,
|
||||||
|
`<p>Citation needed<sup id="fnref:user-content-0"><a href="#fn:user-content-0" rel="nofollow">1</a></sup>.</p>
|
||||||
|
<div>
|
||||||
|
<hr/>
|
||||||
|
<ol>
|
||||||
|
<li id="fn:user-content-0">
|
||||||
|
<p>Source <a href="#fnref:user-content-0" rel="nofollow">↩︎</a></p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^]
|
||||||
|
[^]: Source`,
|
||||||
|
`<p>Citation needed[^]<br/>
|
||||||
|
[^]: Source</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^0]
|
||||||
|
[^0] Source`,
|
||||||
|
`<p>Citation needed[^0]<br/>
|
||||||
|
[^0] Source</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^0]
|
||||||
|
[^0 Source`,
|
||||||
|
`<p>Citation needed[^0]<br/>
|
||||||
|
[^0 Source</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^0] [^0]: Source`,
|
||||||
|
`<p>Citation needed[^0] [^0]: Source</p>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^Source here 0 # 9-3]
|
||||||
|
[^Source here 0 # 9-3]: Source`,
|
||||||
|
`<p>Citation needed<sup id="fnref:user-content-source-here-0-9-3"><a href="#fn:user-content-source-here-0-9-3" rel="nofollow">1</a></sup></p>
|
||||||
|
<div>
|
||||||
|
<hr/>
|
||||||
|
<ol>
|
||||||
|
<li id="fn:user-content-source-here-0-9-3">
|
||||||
|
<p>Source <a href="#fnref:user-content-source-here-0-9-3" rel="nofollow">↩︎</a></p>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
`Citation needed[^0]
|
||||||
|
[^0]:`,
|
||||||
|
`<p>Citation needed<sup id="fnref:user-content-0"><a href="#fn:user-content-0" rel="nofollow">1</a></sup></p>
|
||||||
|
<div>
|
||||||
|
<hr/>
|
||||||
|
<ol>
|
||||||
|
<li id="fn:user-content-0">
|
||||||
|
<a href="#fnref:user-content-0" rel="nofollow">↩︎</a></li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range testcases {
|
||||||
|
res, err := markdown.RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase)
|
||||||
|
assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase)
|
||||||
|
assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTaskList(t *testing.T) {
|
func TestTaskList(t *testing.T) {
|
||||||
testcases := []struct {
|
testcases := []struct {
|
||||||
testcase string
|
testcase string
|
||||||
|
|
|
@ -55,10 +55,7 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
|
||||||
return node, parser.Close | parser.NoChildren
|
return node, parser.Close | parser.NoChildren
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.Advance(segment.Len() - 1)
|
return nil, parser.NoChildren
|
||||||
segment.Start += 2
|
|
||||||
node.Lines().Append(segment)
|
|
||||||
return node, parser.NoChildren
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue parses the current line and returns a result of parsing.
|
// Continue parses the current line and returns a result of parsing.
|
||||||
|
|
|
@ -7,13 +7,12 @@ import (
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// AesEncrypt encrypts text and given key with AES.
|
// AesEncrypt encrypts text and given key with AES.
|
||||||
|
|
|
@ -5,8 +5,9 @@ package setting
|
||||||
|
|
||||||
// Admin settings
|
// Admin settings
|
||||||
var Admin struct {
|
var Admin struct {
|
||||||
DisableRegularOrgCreation bool
|
DisableRegularOrgCreation bool
|
||||||
DefaultEmailNotification string
|
DefaultEmailNotification string
|
||||||
|
SendNotificationEmailOnNewUser bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadAdminFrom(rootCfg ConfigProvider) {
|
func loadAdminFrom(rootCfg ConfigProvider) {
|
||||||
|
|
24
modules/setting/badges.go
Normal file
24
modules/setting/badges.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Badges settings
|
||||||
|
var Badges = struct {
|
||||||
|
Enabled bool `ini:"ENABLED"`
|
||||||
|
GeneratorURLTemplate string `ini:"GENERATOR_URL_TEMPLATE"`
|
||||||
|
GeneratorURLTemplateTemplate *template.Template `ini:"-"`
|
||||||
|
}{
|
||||||
|
Enabled: true,
|
||||||
|
GeneratorURLTemplate: "https://img.shields.io/badge/{{.label}}-{{.text}}-{{.color}}",
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadBadgesFrom(rootCfg ConfigProvider) {
|
||||||
|
mustMapSetting(rootCfg, "badges", &Badges)
|
||||||
|
|
||||||
|
Badges.GeneratorURLTemplateTemplate = template.Must(template.New("").Parse(Badges.GeneratorURLTemplate))
|
||||||
|
}
|
|
@ -45,6 +45,7 @@ var (
|
||||||
ConnMaxLifetime time.Duration
|
ConnMaxLifetime time.Duration
|
||||||
IterateBufferSize int
|
IterateBufferSize int
|
||||||
AutoMigration bool
|
AutoMigration bool
|
||||||
|
SlowQueryTreshold time.Duration
|
||||||
}{
|
}{
|
||||||
Timeout: 500,
|
Timeout: 500,
|
||||||
IterateBufferSize: 50,
|
IterateBufferSize: 50,
|
||||||
|
@ -87,6 +88,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
|
||||||
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
|
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
|
||||||
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
|
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
|
||||||
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
|
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
|
||||||
|
Database.SlowQueryTreshold = sec.Key("SLOW_QUERY_TRESHOLD").MustDuration(5 * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DBConnStr returns database connection string
|
// DBConnStr returns database connection string
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
@ -19,6 +20,8 @@ const (
|
||||||
RepoCreatingPublic = "public"
|
RepoCreatingPublic = "public"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var RecognisedRepositoryDownloadOrCloneMethods = []string{"download-zip", "download-targz", "download-bundle", "vscode-clone", "vscodium-clone", "cite"}
|
||||||
|
|
||||||
// ItemsPerPage maximum items per page in forks, watchers and stars of a repo
|
// ItemsPerPage maximum items per page in forks, watchers and stars of a repo
|
||||||
const ItemsPerPage = 40
|
const ItemsPerPage = 40
|
||||||
|
|
||||||
|
@ -43,6 +46,7 @@ var (
|
||||||
DisabledRepoUnits []string
|
DisabledRepoUnits []string
|
||||||
DefaultRepoUnits []string
|
DefaultRepoUnits []string
|
||||||
DefaultForkRepoUnits []string
|
DefaultForkRepoUnits []string
|
||||||
|
DownloadOrCloneMethods []string
|
||||||
PrefixArchiveFiles bool
|
PrefixArchiveFiles bool
|
||||||
DisableMigrations bool
|
DisableMigrations bool
|
||||||
DisableStars bool `ini:"DISABLE_STARS"`
|
DisableStars bool `ini:"DISABLE_STARS"`
|
||||||
|
@ -108,6 +112,9 @@ var (
|
||||||
Wiki []string
|
Wiki []string
|
||||||
DefaultTrustModel string
|
DefaultTrustModel string
|
||||||
} `ini:"repository.signing"`
|
} `ini:"repository.signing"`
|
||||||
|
|
||||||
|
SettableFlags []string
|
||||||
|
EnableFlags bool
|
||||||
}{
|
}{
|
||||||
DetectedCharsetsOrder: []string{
|
DetectedCharsetsOrder: []string{
|
||||||
"UTF-8",
|
"UTF-8",
|
||||||
|
@ -150,7 +157,7 @@ var (
|
||||||
DefaultPrivate: RepoCreatingLastUserVisibility,
|
DefaultPrivate: RepoCreatingLastUserVisibility,
|
||||||
DefaultPushCreatePrivate: true,
|
DefaultPushCreatePrivate: true,
|
||||||
MaxCreationLimit: -1,
|
MaxCreationLimit: -1,
|
||||||
PreferredLicenses: []string{"Apache License 2.0", "MIT License"},
|
PreferredLicenses: []string{"Apache-2.0", "MIT"},
|
||||||
DisableHTTPGit: false,
|
DisableHTTPGit: false,
|
||||||
AccessControlAllowOrigin: "",
|
AccessControlAllowOrigin: "",
|
||||||
UseCompatSSHURI: false,
|
UseCompatSSHURI: false,
|
||||||
|
@ -160,6 +167,7 @@ var (
|
||||||
DisabledRepoUnits: []string{},
|
DisabledRepoUnits: []string{},
|
||||||
DefaultRepoUnits: []string{},
|
DefaultRepoUnits: []string{},
|
||||||
DefaultForkRepoUnits: []string{},
|
DefaultForkRepoUnits: []string{},
|
||||||
|
DownloadOrCloneMethods: []string{"download-zip", "download-targz", "download-bundle", "vscode-clone"},
|
||||||
PrefixArchiveFiles: true,
|
PrefixArchiveFiles: true,
|
||||||
DisableMigrations: false,
|
DisableMigrations: false,
|
||||||
DisableStars: false,
|
DisableStars: false,
|
||||||
|
@ -262,6 +270,8 @@ var (
|
||||||
Wiki: []string{"never"},
|
Wiki: []string{"never"},
|
||||||
DefaultTrustModel: "collaborator",
|
DefaultTrustModel: "collaborator",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
EnableFlags: false,
|
||||||
}
|
}
|
||||||
RepoRootPath string
|
RepoRootPath string
|
||||||
ScriptType = "bash"
|
ScriptType = "bash"
|
||||||
|
@ -358,4 +368,12 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
|
||||||
if err := loadRepoArchiveFrom(rootCfg); err != nil {
|
if err := loadRepoArchiveFrom(rootCfg); err != nil {
|
||||||
log.Fatal("loadRepoArchiveFrom: %v", err)
|
log.Fatal("loadRepoArchiveFrom: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, method := range Repository.DownloadOrCloneMethods {
|
||||||
|
if !slices.Contains(RecognisedRepositoryDownloadOrCloneMethods, method) {
|
||||||
|
log.Error("Unrecognised repository download or clone method: %s", method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool()
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,7 @@ var Service = struct {
|
||||||
DefaultKeepEmailPrivate bool
|
DefaultKeepEmailPrivate bool
|
||||||
DefaultAllowCreateOrganization bool
|
DefaultAllowCreateOrganization bool
|
||||||
DefaultUserIsRestricted bool
|
DefaultUserIsRestricted bool
|
||||||
|
AllowDotsInUsernames bool
|
||||||
EnableTimetracking bool
|
EnableTimetracking bool
|
||||||
DefaultEnableTimetracking bool
|
DefaultEnableTimetracking bool
|
||||||
DefaultEnableDependencies bool
|
DefaultEnableDependencies bool
|
||||||
|
@ -180,6 +181,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
||||||
Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
|
Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
|
||||||
Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true)
|
Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true)
|
||||||
Service.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false)
|
Service.DefaultUserIsRestricted = sec.Key("DEFAULT_USER_IS_RESTRICTED").MustBool(false)
|
||||||
|
Service.AllowDotsInUsernames = sec.Key("ALLOW_DOTS_IN_USERNAMES").MustBool(true)
|
||||||
Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true)
|
Service.EnableTimetracking = sec.Key("ENABLE_TIMETRACKING").MustBool(true)
|
||||||
if Service.EnableTimetracking {
|
if Service.EnableTimetracking {
|
||||||
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
|
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
|
||||||
|
|
|
@ -147,6 +147,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
||||||
loadUIFrom(cfg)
|
loadUIFrom(cfg)
|
||||||
loadAdminFrom(cfg)
|
loadAdminFrom(cfg)
|
||||||
loadAPIFrom(cfg)
|
loadAPIFrom(cfg)
|
||||||
|
loadBadgesFrom(cfg)
|
||||||
loadMetricsFrom(cfg)
|
loadMetricsFrom(cfg)
|
||||||
loadCamoFrom(cfg)
|
loadCamoFrom(cfg)
|
||||||
loadI18nFrom(cfg)
|
loadI18nFrom(cfg)
|
||||||
|
|
|
@ -402,6 +402,16 @@ func (p *PullRequestPayload) JSONPayload() ([]byte, error) {
|
||||||
return json.MarshalIndent(p, "", " ")
|
return json.MarshalIndent(p, "", " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HookScheduleAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
HookScheduleCreated HookScheduleAction = "schedule"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SchedulePayload struct {
|
||||||
|
Action HookScheduleAction `json:"action"`
|
||||||
|
}
|
||||||
|
|
||||||
// ReviewPayload FIXME
|
// ReviewPayload FIXME
|
||||||
type ReviewPayload struct {
|
type ReviewPayload struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
|
@ -89,6 +89,9 @@ type CreatePullReviewComment struct {
|
||||||
NewLineNum int64 `json:"new_position"`
|
NewLineNum int64 `json:"new_position"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreatePullReviewCommentOptions are options to create a pull review comment
|
||||||
|
type CreatePullReviewCommentOptions CreatePullReviewComment
|
||||||
|
|
||||||
// SubmitPullReviewOptions are options to submit a pending pull review
|
// SubmitPullReviewOptions are options to submit a pending pull review
|
||||||
type SubmitPullReviewOptions struct {
|
type SubmitPullReviewOptions struct {
|
||||||
Event ReviewStateType `json:"event"`
|
Event ReviewStateType `json:"event"`
|
||||||
|
|
9
modules/structs/repo_flags.go
Normal file
9
modules/structs/repo_flags.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package structs
|
||||||
|
|
||||||
|
// ReplaceFlagsOption options when replacing the flags of a repository
|
||||||
|
type ReplaceFlagsOption struct {
|
||||||
|
Flags []string `json:"flags"`
|
||||||
|
}
|
|
@ -96,6 +96,9 @@ func NewFuncMap() template.FuncMap {
|
||||||
"AppDomain": func() string { // documented in mail-templates.md
|
"AppDomain": func() string { // documented in mail-templates.md
|
||||||
return setting.Domain
|
return setting.Domain
|
||||||
},
|
},
|
||||||
|
"RepoFlagsEnabled": func() bool {
|
||||||
|
return setting.Repository.EnableFlags
|
||||||
|
},
|
||||||
"AssetVersion": func() string {
|
"AssetVersion": func() string {
|
||||||
return setting.AssetVersion
|
return setting.AssetVersion
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,10 +7,9 @@ import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateKeyPair generates a public and private keypair
|
// GenerateKeyPair generates a public and private keypair
|
||||||
|
|
|
@ -7,12 +7,12 @@ import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -117,13 +117,20 @@ func IsValidExternalTrackerURLFormat(uri string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
validUsernamePattern = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`)
|
validUsernamePatternWithDots = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`)
|
||||||
invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) // No consecutive or trailing non-alphanumeric chars
|
validUsernamePatternWithoutDots = regexp.MustCompile(`^[\da-zA-Z][-\w]*$`)
|
||||||
|
|
||||||
|
// No consecutive or trailing non-alphanumeric chars, catches both cases
|
||||||
|
invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsValidUsername checks if username is valid
|
// IsValidUsername checks if username is valid
|
||||||
func IsValidUsername(name string) bool {
|
func IsValidUsername(name string) bool {
|
||||||
// It is difficult to find a single pattern that is both readable and effective,
|
// It is difficult to find a single pattern that is both readable and effective,
|
||||||
// but it's easier to use positive and negative checks.
|
// but it's easier to use positive and negative checks.
|
||||||
return validUsernamePattern.MatchString(name) && !invalidUsernamePattern.MatchString(name)
|
if setting.Service.AllowDotsInUsernames {
|
||||||
|
return validUsernamePatternWithDots.MatchString(name) && !invalidUsernamePattern.MatchString(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return validUsernamePatternWithoutDots.MatchString(name) && !invalidUsernamePattern.MatchString(name)
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,7 +155,8 @@ func Test_IsValidExternalTrackerURLFormat(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsValidUsername(t *testing.T) {
|
func TestIsValidUsernameAllowDots(t *testing.T) {
|
||||||
|
setting.Service.AllowDotsInUsernames = true
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
arg string
|
arg string
|
||||||
want bool
|
want bool
|
||||||
|
@ -185,3 +186,31 @@ func TestIsValidUsername(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsValidUsernameBanDots(t *testing.T) {
|
||||||
|
setting.Service.AllowDotsInUsernames = false
|
||||||
|
defer func() {
|
||||||
|
setting.Service.AllowDotsInUsernames = true
|
||||||
|
}()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
arg string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{arg: "a", want: true},
|
||||||
|
{arg: "abc", want: true},
|
||||||
|
{arg: "0.b-c", want: false},
|
||||||
|
{arg: "a.b-c_d", want: false},
|
||||||
|
{arg: ".abc", want: false},
|
||||||
|
{arg: "abc.", want: false},
|
||||||
|
{arg: "a..bc", want: false},
|
||||||
|
{arg: "a...bc", want: false},
|
||||||
|
{arg: "a.-bc", want: false},
|
||||||
|
{arg: "a._bc", want: false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.arg, func(t *testing.T) {
|
||||||
|
assert.Equalf(t, tt.want, IsValidUsername(tt.arg), "IsValidUsername[AllowDotsInUsernames=false](%v)", tt.arg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -147,6 +147,16 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hp, ok := handler.(func(next http.Handler) http.HandlerFunc); ok {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info
|
||||||
|
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||||
|
h.ServeHTTP(resp, req)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
provider := func(next http.Handler) http.Handler {
|
provider := func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
|
||||||
// wrap the response writer to check whether the response has been written
|
// wrap the response writer to check whether the response has been written
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/validation"
|
"code.gitea.io/gitea/modules/validation"
|
||||||
|
@ -135,7 +136,11 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
|
||||||
case validation.ErrRegexPattern:
|
case validation.ErrRegexPattern:
|
||||||
data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
|
data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
|
||||||
case validation.ErrUsername:
|
case validation.ErrUsername:
|
||||||
data["ErrorMsg"] = trName + l.Tr("form.username_error")
|
if setting.Service.AllowDotsInUsernames {
|
||||||
|
data["ErrorMsg"] = trName + l.Tr("form.username_error")
|
||||||
|
} else {
|
||||||
|
data["ErrorMsg"] = trName + l.Tr("form.username_error_no_dots")
|
||||||
|
}
|
||||||
case validation.ErrInvalidGroupTeamMap:
|
case validation.ErrInvalidGroupTeamMap:
|
||||||
data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
|
data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -53,6 +53,7 @@ func CommonTemplateContextData() ContextData {
|
||||||
"ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage,
|
"ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage,
|
||||||
"ShowFooterVersion": setting.Other.ShowFooterVersion,
|
"ShowFooterVersion": setting.Other.ShowFooterVersion,
|
||||||
"DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives,
|
"DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives,
|
||||||
|
"DownloadOrCloneMethods": setting.Repository.DownloadOrCloneMethods,
|
||||||
|
|
||||||
"EnableSwagger": setting.API.EnableSwagger,
|
"EnableSwagger": setting.API.EnableSwagger,
|
||||||
"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn,
|
"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn,
|
||||||
|
|
|
@ -295,6 +295,7 @@ default_allow_create_organization = Allow Creation of Organizations by Default
|
||||||
default_allow_create_organization_popup = Allow new user accounts to create organizations by default.
|
default_allow_create_organization_popup = Allow new user accounts to create organizations by default.
|
||||||
default_enable_timetracking = Enable Time Tracking by Default
|
default_enable_timetracking = Enable Time Tracking by Default
|
||||||
default_enable_timetracking_popup = Enable time tracking for new repositories by default.
|
default_enable_timetracking_popup = Enable time tracking for new repositories by default.
|
||||||
|
allow_dots_in_usernames = Allow users to use dots in their usernames. Doesn't affect existing accounts.
|
||||||
no_reply_address = Hidden Email Domain
|
no_reply_address = Hidden Email Domain
|
||||||
no_reply_address_helper = Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'.
|
no_reply_address_helper = Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'.
|
||||||
password_algorithm = Password Hash Algorithm
|
password_algorithm = Password Hash Algorithm
|
||||||
|
@ -367,7 +368,7 @@ forgot_password_title= Forgot Password
|
||||||
forgot_password = Forgot password?
|
forgot_password = Forgot password?
|
||||||
sign_up_now = Need an account? Register now.
|
sign_up_now = Need an account? Register now.
|
||||||
sign_up_successful = Account was successfully created. Welcome!
|
sign_up_successful = Account was successfully created. Welcome!
|
||||||
confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process.
|
confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. If the email is incorrect, you can log in, and request another confirmation email to be sent to a different address.
|
||||||
must_change_password = Update your password
|
must_change_password = Update your password
|
||||||
allow_password_change = Require user to change password (recommended)
|
allow_password_change = Require user to change password (recommended)
|
||||||
reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the account recovery process.
|
reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the account recovery process.
|
||||||
|
@ -377,6 +378,9 @@ prohibit_login = Sign In Prohibited
|
||||||
prohibit_login_desc = Your account is prohibited from signing in, please contact your site administrator.
|
prohibit_login_desc = Your account is prohibited from signing in, please contact your site administrator.
|
||||||
resent_limit_prompt = You have already requested an activation email recently. Please wait 3 minutes and try again.
|
resent_limit_prompt = You have already requested an activation email recently. Please wait 3 minutes and try again.
|
||||||
has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to resend a new one, please click on the button below.
|
has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to resend a new one, please click on the button below.
|
||||||
|
change_unconfirmed_email_summary = Change the email address activation mail is sent to.
|
||||||
|
change_unconfirmed_email = If you have given the wrong email address during registration, you can change it below, and a confirmation will be sent to the new address instead.
|
||||||
|
change_unconfirmed_email_error = Unable to change the email address: %v
|
||||||
resend_mail = Click here to resend your activation email
|
resend_mail = Click here to resend your activation email
|
||||||
email_not_associate = The email address is not associated with any account.
|
email_not_associate = The email address is not associated with any account.
|
||||||
send_reset_mail = Send Account Recovery Email
|
send_reset_mail = Send Account Recovery Email
|
||||||
|
@ -441,6 +445,10 @@ activate_email = Verify your email address
|
||||||
activate_email.title = %s, please verify your email address
|
activate_email.title = %s, please verify your email address
|
||||||
activate_email.text = Please click the following link to verify your email address within <b>%s</b>:
|
activate_email.text = Please click the following link to verify your email address within <b>%s</b>:
|
||||||
|
|
||||||
|
admin.new_user.subject = New user %s just signed up
|
||||||
|
admin.new_user.user_info = User Information
|
||||||
|
admin.new_user.text = Please <a href="%s">click here</a> to manage the user from the admin panel.
|
||||||
|
|
||||||
register_notify = Welcome to Gitea
|
register_notify = Welcome to Gitea
|
||||||
register_notify.title = %[1]s, welcome to %[2]s
|
register_notify.title = %[1]s, welcome to %[2]s
|
||||||
register_notify.text_1 = this is your registration confirmation email for %s!
|
register_notify.text_1 = this is your registration confirmation email for %s!
|
||||||
|
@ -535,6 +543,7 @@ include_error = ` must contain substring "%s".`
|
||||||
glob_pattern_error = ` glob pattern is invalid: %s.`
|
glob_pattern_error = ` glob pattern is invalid: %s.`
|
||||||
regex_pattern_error = ` regex pattern is invalid: %s.`
|
regex_pattern_error = ` regex pattern is invalid: %s.`
|
||||||
username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.`
|
username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.`
|
||||||
|
username_error_no_dots = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-') and underscore ('_'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.`
|
||||||
invalid_group_team_map_error = ` mapping is invalid: %s`
|
invalid_group_team_map_error = ` mapping is invalid: %s`
|
||||||
unknown_error = Unknown error:
|
unknown_error = Unknown error:
|
||||||
captcha_incorrect = The CAPTCHA code is incorrect.
|
captcha_incorrect = The CAPTCHA code is incorrect.
|
||||||
|
@ -944,6 +953,14 @@ user_unblock_success = The user has been unblocked successfully.
|
||||||
user_block_success = The user has been blocked successfully.
|
user_block_success = The user has been blocked successfully.
|
||||||
|
|
||||||
[repo]
|
[repo]
|
||||||
|
rss.must_be_on_branch = You must be on a branch to have an RSS feed.
|
||||||
|
|
||||||
|
admin.manage_flags = Manage flags
|
||||||
|
admin.enabled_flags = Flags enabled for the repository:
|
||||||
|
admin.update_flags = Update flags
|
||||||
|
admin.failed_to_replace_flags = Failed to replace repository flags
|
||||||
|
admin.flags_replaced = Repository flags replaced
|
||||||
|
|
||||||
new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? <a href="%s">Migrate repository.</a>
|
new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? <a href="%s">Migrate repository.</a>
|
||||||
owner = Owner
|
owner = Owner
|
||||||
owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit.
|
owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit.
|
||||||
|
@ -970,6 +987,7 @@ all_branches = All branches
|
||||||
fork_no_valid_owners = This repository can not be forked because there are no valid owners.
|
fork_no_valid_owners = This repository can not be forked because there are no valid owners.
|
||||||
use_template = Use this template
|
use_template = Use this template
|
||||||
clone_in_vsc = Clone in VS Code
|
clone_in_vsc = Clone in VS Code
|
||||||
|
clone_in_vscodium = Clone in VS Codium
|
||||||
download_zip = Download ZIP
|
download_zip = Download ZIP
|
||||||
download_tar = Download TAR.GZ
|
download_tar = Download TAR.GZ
|
||||||
download_bundle = Download BUNDLE
|
download_bundle = Download BUNDLE
|
||||||
|
@ -1256,6 +1274,7 @@ editor.new_branch_name_desc = New branch name…
|
||||||
editor.cancel = Cancel
|
editor.cancel = Cancel
|
||||||
editor.filename_cannot_be_empty = The filename cannot be empty.
|
editor.filename_cannot_be_empty = The filename cannot be empty.
|
||||||
editor.filename_is_invalid = The filename is invalid: "%s".
|
editor.filename_is_invalid = The filename is invalid: "%s".
|
||||||
|
editor.invalid_commit_mail = Invalid mail for creating a commit.
|
||||||
editor.branch_does_not_exist = Branch "%s" does not exist in this repository.
|
editor.branch_does_not_exist = Branch "%s" does not exist in this repository.
|
||||||
editor.branch_already_exists = Branch "%s" already exists in this repository.
|
editor.branch_already_exists = Branch "%s" already exists in this repository.
|
||||||
editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository.
|
editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository.
|
||||||
|
@ -1294,6 +1313,8 @@ commits.find = Search
|
||||||
commits.search_all = All Branches
|
commits.search_all = All Branches
|
||||||
commits.author = Author
|
commits.author = Author
|
||||||
commits.message = Message
|
commits.message = Message
|
||||||
|
commits.browse_further = Browse further
|
||||||
|
commits.renamed_from = Renamed from %s
|
||||||
commits.date = Date
|
commits.date = Date
|
||||||
commits.older = Older
|
commits.older = Older
|
||||||
commits.newer = Newer
|
commits.newer = Newer
|
||||||
|
@ -1843,7 +1864,7 @@ pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull re
|
||||||
pulls.delete.title = Delete this pull request?
|
pulls.delete.title = Delete this pull request?
|
||||||
pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived)
|
pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived)
|
||||||
|
|
||||||
pulls.recently_pushed_new_branches = You pushed on branch <strong>%[1]s</strong> %[2]s
|
pulls.recently_pushed_new_branches = You pushed on branch <a href="%[3]s"><strong>%[1]s</strong></a> %[2]s
|
||||||
|
|
||||||
pull.deleted_branch = (deleted):%s
|
pull.deleted_branch = (deleted):%s
|
||||||
|
|
||||||
|
@ -1906,6 +1927,7 @@ wiki.page_title = Page title
|
||||||
wiki.page_content = Page content
|
wiki.page_content = Page content
|
||||||
wiki.default_commit_message = Write a note about this page update (optional).
|
wiki.default_commit_message = Write a note about this page update (optional).
|
||||||
wiki.save_page = Save Page
|
wiki.save_page = Save Page
|
||||||
|
wiki.cancel = Cancel
|
||||||
wiki.last_commit_info = %s edited this page %s
|
wiki.last_commit_info = %s edited this page %s
|
||||||
wiki.edit_page_button = Edit
|
wiki.edit_page_button = Edit
|
||||||
wiki.new_page_button = New Page
|
wiki.new_page_button = New Page
|
||||||
|
@ -2044,6 +2066,7 @@ settings.branches.update_default_branch = Update Default Branch
|
||||||
settings.branches.add_new_rule = Add New Rule
|
settings.branches.add_new_rule = Add New Rule
|
||||||
settings.advanced_settings = Advanced Settings
|
settings.advanced_settings = Advanced Settings
|
||||||
settings.wiki_desc = Enable Repository Wiki
|
settings.wiki_desc = Enable Repository Wiki
|
||||||
|
settings.wiki_globally_editable = Allow anyone to edit the Wiki
|
||||||
settings.use_internal_wiki = Use Built-In Wiki
|
settings.use_internal_wiki = Use Built-In Wiki
|
||||||
settings.use_external_wiki = Use External Wiki
|
settings.use_external_wiki = Use External Wiki
|
||||||
settings.external_wiki_url = External Wiki URL
|
settings.external_wiki_url = External Wiki URL
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
@ -26,8 +27,6 @@ import (
|
||||||
chef_module "code.gitea.io/gitea/modules/packages/chef"
|
chef_module "code.gitea.io/gitea/modules/packages/chef"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/services/auth"
|
"code.gitea.io/gitea/services/auth"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -6,6 +6,7 @@ package maven
|
||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
|
@ -26,8 +27,6 @@ import (
|
||||||
maven_module "code.gitea.io/gitea/modules/packages/maven"
|
maven_module "code.gitea.io/gitea/modules/packages/maven"
|
||||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||||
packages_service "code.gitea.io/gitea/services/packages"
|
packages_service "code.gitea.io/gitea/services/packages"
|
||||||
|
|
||||||
"github.com/minio/sha256-simd"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
//
|
//
|
||||||
// This documentation describes the Gitea API.
|
// This documentation describes the Gitea API.
|
||||||
//
|
//
|
||||||
// Schemes: http, https
|
// Schemes: https, http
|
||||||
// BasePath: /api/v1
|
// BasePath: /api/v1
|
||||||
// Version: {{AppVer | JSEscape | Safe}}
|
// Version: {{AppVer | JSEscape | Safe}}
|
||||||
// License: MIT http://opensource.org/licenses/MIT
|
// License: MIT http://opensource.org/licenses/MIT
|
||||||
|
@ -73,6 +73,7 @@ import (
|
||||||
actions_model "code.gitea.io/gitea/models/actions"
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
auth_model "code.gitea.io/gitea/models/auth"
|
auth_model "code.gitea.io/gitea/models/auth"
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
"code.gitea.io/gitea/models/organization"
|
"code.gitea.io/gitea/models/organization"
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
|
@ -230,6 +231,39 @@ func repoAssignment() func(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// must be used within a group with a call to repoAssignment() to set ctx.Repo
|
||||||
|
func commentAssignment(idParam string) func(ctx *context.APIContext) {
|
||||||
|
return func(ctx *context.APIContext) {
|
||||||
|
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(idParam))
|
||||||
|
if err != nil {
|
||||||
|
if issues_model.IsErrCommentNotExist(err) {
|
||||||
|
ctx.NotFound(err)
|
||||||
|
} else {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = comment.LoadIssue(ctx); err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||||
|
ctx.NotFound()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
comment.Issue.Repo = ctx.Repo.Repository
|
||||||
|
|
||||||
|
ctx.Comment = comment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
|
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
|
||||||
return func(ctx *context.APIContext) {
|
return func(ctx *context.APIContext) {
|
||||||
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
|
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
|
||||||
|
@ -1104,6 +1138,18 @@ func Routes() *web.Route {
|
||||||
m.Get("/permission", repo.GetRepoPermissions)
|
m.Get("/permission", repo.GetRepoPermissions)
|
||||||
})
|
})
|
||||||
}, reqToken())
|
}, reqToken())
|
||||||
|
if setting.Repository.EnableFlags {
|
||||||
|
m.Group("/flags", func() {
|
||||||
|
m.Combo("").Get(repo.ListFlags).
|
||||||
|
Put(bind(api.ReplaceFlagsOption{}), repo.ReplaceAllFlags).
|
||||||
|
Delete(repo.DeleteAllFlags)
|
||||||
|
m.Group("/{flag}", func() {
|
||||||
|
m.Combo("").Get(repo.HasFlag).
|
||||||
|
Put(repo.AddFlag).
|
||||||
|
Delete(repo.DeleteFlag)
|
||||||
|
})
|
||||||
|
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
|
||||||
|
}
|
||||||
m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
|
m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
|
||||||
m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
|
m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
|
||||||
m.Group("/teams", func() {
|
m.Group("/teams", func() {
|
||||||
|
@ -1223,8 +1269,12 @@ func Routes() *web.Route {
|
||||||
Get(repo.GetPullReview).
|
Get(repo.GetPullReview).
|
||||||
Delete(reqToken(), repo.DeletePullReview).
|
Delete(reqToken(), repo.DeletePullReview).
|
||||||
Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
|
Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
|
||||||
m.Combo("/comments").
|
m.Group("/comments", func() {
|
||||||
Get(repo.GetPullReviewComments)
|
m.Combo("").
|
||||||
|
Get(repo.GetPullReviewComments).
|
||||||
|
Post(reqToken(), bind(api.CreatePullReviewCommentOptions{}), repo.CreatePullReviewComment)
|
||||||
|
m.Get("/{comment}", commentAssignment("comment"), repo.GetPullReviewComment)
|
||||||
|
})
|
||||||
m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
|
m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
|
||||||
m.Post("/undismissals", reqToken(), repo.UnDismissPullReview)
|
m.Post("/undismissals", reqToken(), repo.UnDismissPullReview)
|
||||||
})
|
})
|
||||||
|
@ -1328,7 +1378,7 @@ func Routes() *web.Route {
|
||||||
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
|
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
|
||||||
Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueCommentAttachment)
|
Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueCommentAttachment)
|
||||||
}, mustEnableAttachments)
|
}, mustEnableAttachments)
|
||||||
})
|
}, commentAssignment(":id"))
|
||||||
})
|
})
|
||||||
m.Group("/{index}", func() {
|
m.Group("/{index}", func() {
|
||||||
m.Combo("").Get(repo.GetIssue).
|
m.Combo("").Get(repo.GetIssue).
|
||||||
|
|
245
routers/api/v1/repo/flags.go
Normal file
245
routers/api/v1/repo/flags.go
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/web"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListFlags(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/flags repository repoListFlags
|
||||||
|
// ---
|
||||||
|
// summary: List a repository's flags
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/StringSlice"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
repoFlags, err := ctx.Repo.Repository.ListFlags(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := make([]string, len(repoFlags))
|
||||||
|
for i := range repoFlags {
|
||||||
|
flags[i] = repoFlags[i].Name
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SetTotalCountHeader(int64(len(repoFlags)))
|
||||||
|
ctx.JSON(http.StatusOK, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReplaceAllFlags(ctx *context.APIContext) {
|
||||||
|
// swagger:operation PUT /repos/{owner}/{repo}/flags repository repoReplaceAllFlags
|
||||||
|
// ---
|
||||||
|
// summary: Replace all flags of a repository
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/ReplaceFlagsOption"
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
flagsForm := web.GetForm(ctx).(*api.ReplaceFlagsOption)
|
||||||
|
|
||||||
|
if err := ctx.Repo.Repository.ReplaceAllFlags(ctx, flagsForm.Flags); err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteAllFlags(ctx *context.APIContext) {
|
||||||
|
// swagger:operation DELETE /repos/{owner}/{repo}/flags repository repoDeleteAllFlags
|
||||||
|
// ---
|
||||||
|
// summary: Remove all flags from a repository
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
if err := ctx.Repo.Repository.ReplaceAllFlags(ctx, nil); err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HasFlag(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/flags/{flag} repository repoCheckFlag
|
||||||
|
// ---
|
||||||
|
// summary: Check if a repository has a given flag
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: flag
|
||||||
|
// in: path
|
||||||
|
// description: name of the flag
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
hasFlag := ctx.Repo.Repository.HasFlag(ctx, ctx.Params(":flag"))
|
||||||
|
if hasFlag {
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
} else {
|
||||||
|
ctx.NotFound()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddFlag(ctx *context.APIContext) {
|
||||||
|
// swagger:operation PUT /repos/{owner}/{repo}/flags/{flag} repository repoAddFlag
|
||||||
|
// ---
|
||||||
|
// summary: Add a flag to a repository
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: flag
|
||||||
|
// in: path
|
||||||
|
// description: name of the flag
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
flag := ctx.Params(":flag")
|
||||||
|
|
||||||
|
if ctx.Repo.Repository.HasFlag(ctx, flag) {
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctx.Repo.Repository.AddFlag(ctx, flag); err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteFlag(ctx *context.APIContext) {
|
||||||
|
// swagger:operation DELETE /repos/{owner}/{repo}/flags/{flag} repository repoDeleteFlag
|
||||||
|
// ---
|
||||||
|
// summary: Remove a flag from a repository
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: flag
|
||||||
|
// in: path
|
||||||
|
// description: name of the flag
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "204":
|
||||||
|
// "$ref": "#/responses/empty"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
flag := ctx.Params(":flag")
|
||||||
|
|
||||||
|
if _, err := ctx.Repo.Repository.DeleteFlag(ctx, flag); err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Status(http.StatusNoContent)
|
||||||
|
}
|
|
@ -454,29 +454,7 @@ func GetIssueComment(ctx *context.APIContext) {
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
|
comment := ctx.Comment
|
||||||
if err != nil {
|
|
||||||
if issues_model.IsErrCommentNotExist(err) {
|
|
||||||
ctx.NotFound(err)
|
|
||||||
} else {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = comment.LoadIssue(ctx); err != nil {
|
|
||||||
ctx.InternalServerError(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
|
||||||
ctx.Status(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
|
||||||
ctx.NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if comment.Type != issues_model.CommentTypeComment {
|
if comment.Type != issues_model.CommentTypeComment {
|
||||||
ctx.Status(http.StatusNoContent)
|
ctx.Status(http.StatusNoContent)
|
||||||
|
@ -587,25 +565,7 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) {
|
func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) {
|
||||||
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
|
comment := ctx.Comment
|
||||||
if err != nil {
|
|
||||||
if issues_model.IsErrCommentNotExist(err) {
|
|
||||||
ctx.NotFound(err)
|
|
||||||
} else {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := comment.LoadIssue(ctx); err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
|
||||||
ctx.Status(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||||
ctx.Status(http.StatusForbidden)
|
ctx.Status(http.StatusForbidden)
|
||||||
|
@ -617,7 +577,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = comment.LoadIssue(ctx)
|
err := comment.LoadIssue(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||||
return
|
return
|
||||||
|
@ -711,25 +671,7 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteIssueComment(ctx *context.APIContext) {
|
func deleteIssueComment(ctx *context.APIContext) {
|
||||||
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
|
comment := ctx.Comment
|
||||||
if err != nil {
|
|
||||||
if issues_model.IsErrCommentNotExist(err) {
|
|
||||||
ctx.NotFound(err)
|
|
||||||
} else {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := comment.LoadIssue(ctx); err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
|
||||||
ctx.Status(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||||
ctx.Status(http.StatusForbidden)
|
ctx.Status(http.StatusForbidden)
|
||||||
|
@ -739,7 +681,7 @@ func deleteIssueComment(ctx *context.APIContext) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
|
if err := issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err)
|
ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,11 +55,8 @@ func GetIssueCommentAttachment(ctx *context.APIContext) {
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
|
|
||||||
comment := getIssueCommentSafe(ctx)
|
comment := ctx.Comment
|
||||||
if comment == nil {
|
attachment := getIssueCommentAttachmentSafeRead(ctx)
|
||||||
return
|
|
||||||
}
|
|
||||||
attachment := getIssueCommentAttachmentSafeRead(ctx, comment)
|
|
||||||
if attachment == nil {
|
if attachment == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -101,10 +98,7 @@ func ListIssueCommentAttachments(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/AttachmentList"
|
// "$ref": "#/responses/AttachmentList"
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/error"
|
// "$ref": "#/responses/error"
|
||||||
comment := getIssueCommentSafe(ctx)
|
comment := ctx.Comment
|
||||||
if comment == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := comment.LoadAttachments(ctx); err != nil {
|
if err := comment.LoadAttachments(ctx); err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
|
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
|
||||||
|
@ -166,14 +160,12 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
||||||
// "$ref": "#/responses/repoArchivedError"
|
// "$ref": "#/responses/repoArchivedError"
|
||||||
|
|
||||||
// Check if comment exists and load comment
|
// Check if comment exists and load comment
|
||||||
comment := getIssueCommentSafe(ctx)
|
|
||||||
if comment == nil {
|
if !canUserWriteIssueCommentAttachment(ctx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !canUserWriteIssueCommentAttachment(ctx, comment) {
|
comment := ctx.Comment
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedAt := ctx.Req.FormValue("updated_at")
|
updatedAt := ctx.Req.FormValue("updated_at")
|
||||||
if len(updatedAt) != 0 {
|
if len(updatedAt) != 0 {
|
||||||
|
@ -341,42 +333,17 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) {
|
||||||
ctx.Status(http.StatusNoContent)
|
ctx.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
|
|
||||||
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64("id"))
|
|
||||||
if err != nil {
|
|
||||||
ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := comment.LoadIssue(ctx); err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
|
||||||
ctx.Error(http.StatusNotFound, "", "no matching issue comment found")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
comment.Issue.Repo = ctx.Repo.Repository
|
|
||||||
|
|
||||||
return comment
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
|
func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
|
||||||
comment := getIssueCommentSafe(ctx)
|
if !canUserWriteIssueCommentAttachment(ctx) {
|
||||||
if comment == nil {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !canUserWriteIssueCommentAttachment(ctx, comment) {
|
return getIssueCommentAttachmentSafeRead(ctx)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return getIssueCommentAttachmentSafeRead(ctx, comment)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool {
|
func canUserWriteIssueCommentAttachment(ctx *context.APIContext) bool {
|
||||||
|
// ctx.Comment is assumed to be set in a safe way via a middleware
|
||||||
|
comment := ctx.Comment
|
||||||
|
|
||||||
canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)
|
canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)
|
||||||
if !canEditComment {
|
if !canEditComment {
|
||||||
ctx.Error(http.StatusForbidden, "", "user should have permission to edit comment")
|
ctx.Error(http.StatusForbidden, "", "user should have permission to edit comment")
|
||||||
|
@ -386,7 +353,10 @@ func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment {
|
func getIssueCommentAttachmentSafeRead(ctx *context.APIContext) *repo_model.Attachment {
|
||||||
|
// ctx.Comment is assumed to be set in a safe way via a middleware
|
||||||
|
comment := ctx.Comment
|
||||||
|
|
||||||
attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("attachment_id"))
|
attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("attachment_id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err)
|
ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err)
|
||||||
|
|
|
@ -51,30 +51,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
|
||||||
// "404":
|
// "404":
|
||||||
// "$ref": "#/responses/notFound"
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
|
comment := ctx.Comment
|
||||||
if err != nil {
|
|
||||||
if issues_model.IsErrCommentNotExist(err) {
|
|
||||||
ctx.NotFound(err)
|
|
||||||
} else {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := comment.LoadIssue(ctx); err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
|
||||||
ctx.NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
|
||||||
ctx.Error(http.StatusForbidden, "GetIssueCommentReactions", errors.New("no permission to get reactions"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID)
|
reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -188,30 +165,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
|
func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
|
||||||
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
|
comment := ctx.Comment
|
||||||
if err != nil {
|
|
||||||
if issues_model.IsErrCommentNotExist(err) {
|
|
||||||
ctx.NotFound(err)
|
|
||||||
} else {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = comment.LoadIssue(ctx); err != nil {
|
|
||||||
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
|
||||||
ctx.NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
|
||||||
ctx.NotFound()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
|
if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
|
||||||
ctx.Error(http.StatusForbidden, "ChangeIssueCommentReaction", errors.New("no permission to change reaction"))
|
ctx.Error(http.StatusForbidden, "ChangeIssueCommentReaction", errors.New("no permission to change reaction"))
|
||||||
|
@ -243,7 +197,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// DeleteIssueCommentReaction part
|
// DeleteIssueCommentReaction part
|
||||||
err = issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
|
err := issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "DeleteCommentReaction", err)
|
ctx.Error(http.StatusInternalServerError, "DeleteCommentReaction", err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -208,6 +208,160 @@ func GetPullReviewComments(ctx *context.APIContext) {
|
||||||
ctx.JSON(http.StatusOK, apiComments)
|
ctx.JSON(http.StatusOK, apiComments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPullReviewComment get a pull review comment
|
||||||
|
func GetPullReviewComment(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment} repository repoGetPullReviewComment
|
||||||
|
// ---
|
||||||
|
// summary: Get a pull review comment
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: index
|
||||||
|
// in: path
|
||||||
|
// description: index of the pull request
|
||||||
|
// type: integer
|
||||||
|
// format: int64
|
||||||
|
// required: true
|
||||||
|
// - name: id
|
||||||
|
// in: path
|
||||||
|
// description: id of the review
|
||||||
|
// type: integer
|
||||||
|
// format: int64
|
||||||
|
// required: true
|
||||||
|
// - name: comment
|
||||||
|
// in: path
|
||||||
|
// description: id of the comment
|
||||||
|
// type: integer
|
||||||
|
// format: int64
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/PullReviewComment"
|
||||||
|
// "403":
|
||||||
|
// "$ref": "#/responses/forbidden"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
|
||||||
|
review, _, statusSet := prepareSingleReview(ctx)
|
||||||
|
if statusSet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctx.Comment.LoadPoster(ctx); err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiComment, err := convert.ToPullReviewComment(ctx, review, ctx.Comment, ctx.Doer)
|
||||||
|
if err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, apiComment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePullReviewComments add a new comment to a pull request review
|
||||||
|
func CreatePullReviewComment(ctx *context.APIContext) {
|
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments repository repoCreatePullReviewComment
|
||||||
|
// ---
|
||||||
|
// summary: Add a new comment to a pull request review
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: index
|
||||||
|
// in: path
|
||||||
|
// description: index of the pull request
|
||||||
|
// type: integer
|
||||||
|
// format: int64
|
||||||
|
// required: true
|
||||||
|
// - name: id
|
||||||
|
// in: path
|
||||||
|
// description: id of the review
|
||||||
|
// type: integer
|
||||||
|
// format: int64
|
||||||
|
// required: true
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// required: true
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/CreatePullReviewCommentOptions"
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/PullReviewComment"
|
||||||
|
// "404":
|
||||||
|
// "$ref": "#/responses/notFound"
|
||||||
|
// "422":
|
||||||
|
// "$ref": "#/responses/validationError"
|
||||||
|
|
||||||
|
opts := web.GetForm(ctx).(*api.CreatePullReviewCommentOptions)
|
||||||
|
|
||||||
|
review, pr, statusSet := prepareSingleReview(ctx)
|
||||||
|
if statusSet {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pr.Issue.LoadRepo(ctx); err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
line := opts.NewLineNum
|
||||||
|
if opts.OldLineNum > 0 {
|
||||||
|
line = opts.OldLineNum * -1
|
||||||
|
}
|
||||||
|
|
||||||
|
comment, err := pull_service.CreateCodeComment(ctx,
|
||||||
|
ctx.Doer,
|
||||||
|
ctx.Repo.GitRepo,
|
||||||
|
pr.Issue,
|
||||||
|
line,
|
||||||
|
opts.Body,
|
||||||
|
opts.Path,
|
||||||
|
// as of e522e774cae2240279fc48c349fc513c9d3353ee
|
||||||
|
// isPending is not needed because review.ID is always available
|
||||||
|
// and does not need to be discovered implicitly
|
||||||
|
false,
|
||||||
|
review.ID,
|
||||||
|
// as of e522e774cae2240279fc48c349fc513c9d3353ee
|
||||||
|
// latestCommitID is not needed because it is only used to
|
||||||
|
// create a new review in case it does not already exist
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiComment, err := convert.ToPullReviewComment(ctx, review, comment, ctx.Doer)
|
||||||
|
if err != nil {
|
||||||
|
ctx.InternalServerError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSON(http.StatusOK, apiComment)
|
||||||
|
}
|
||||||
|
|
||||||
// DeletePullReview delete a specific review from a pull request
|
// DeletePullReview delete a specific review from a pull request
|
||||||
func DeletePullReview(ctx *context.APIContext) {
|
func DeletePullReview(ctx *context.APIContext) {
|
||||||
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview
|
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview
|
||||||
|
|
|
@ -17,6 +17,9 @@ type swaggerParameterBodies struct {
|
||||||
// in:body
|
// in:body
|
||||||
AddCollaboratorOption api.AddCollaboratorOption
|
AddCollaboratorOption api.AddCollaboratorOption
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
ReplaceFlagsOption api.ReplaceFlagsOption
|
||||||
|
|
||||||
// in:body
|
// in:body
|
||||||
CreateEmailOption api.CreateEmailOption
|
CreateEmailOption api.CreateEmailOption
|
||||||
// in:body
|
// in:body
|
||||||
|
@ -158,6 +161,9 @@ type swaggerParameterBodies struct {
|
||||||
// in:body
|
// in:body
|
||||||
CreatePullReviewComment api.CreatePullReviewComment
|
CreatePullReviewComment api.CreatePullReviewComment
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
CreatePullReviewCommentOptions api.CreatePullReviewCommentOptions
|
||||||
|
|
||||||
// in:body
|
// in:body
|
||||||
SubmitPullReviewOptions api.SubmitPullReviewOptions
|
SubmitPullReviewOptions api.SubmitPullReviewOptions
|
||||||
|
|
||||||
|
|
|
@ -358,6 +358,12 @@ func SubmitInstall(ctx *context.Context) {
|
||||||
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form)
|
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if len(form.AdminPasswd) < setting.MinPasswordLength {
|
||||||
|
ctx.Data["Err_Admin"] = true
|
||||||
|
ctx.Data["Err_AdminPasswd"] = true
|
||||||
|
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplInstall, form)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init the engine with migration
|
// Init the engine with migration
|
||||||
|
|
|
@ -32,6 +32,7 @@ import (
|
||||||
"code.gitea.io/gitea/services/externalaccount"
|
"code.gitea.io/gitea/services/externalaccount"
|
||||||
"code.gitea.io/gitea/services/forms"
|
"code.gitea.io/gitea/services/forms"
|
||||||
"code.gitea.io/gitea/services/mailer"
|
"code.gitea.io/gitea/services/mailer"
|
||||||
|
notify_service "code.gitea.io/gitea/services/notify"
|
||||||
|
|
||||||
"github.com/markbates/goth"
|
"github.com/markbates/goth"
|
||||||
)
|
)
|
||||||
|
@ -600,6 +601,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notify_service.NewUserSignUp(ctx, u)
|
||||||
// update external user information
|
// update external user information
|
||||||
if gothUser != nil {
|
if gothUser != nil {
|
||||||
if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil {
|
if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil {
|
||||||
|
@ -645,13 +647,22 @@ func Activate(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
// Resend confirmation email.
|
// Resend confirmation email.
|
||||||
if setting.Service.RegisterEmailConfirm {
|
if setting.Service.RegisterEmailConfirm {
|
||||||
if ctx.Cache.IsExist("MailResendLimit_" + ctx.Doer.LowerName) {
|
var cacheKey string
|
||||||
|
if ctx.Cache.IsExist("MailChangedJustNow_" + ctx.Doer.LowerName) {
|
||||||
|
cacheKey = "MailChangedLimit_"
|
||||||
|
if err := ctx.Cache.Delete("MailChangedJustNow_" + ctx.Doer.LowerName); err != nil {
|
||||||
|
log.Error("Delete cache(MailChangedJustNow) fail: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cacheKey = "MailResendLimit_"
|
||||||
|
}
|
||||||
|
if ctx.Cache.IsExist(cacheKey + ctx.Doer.LowerName) {
|
||||||
ctx.Data["ResendLimited"] = true
|
ctx.Data["ResendLimited"] = true
|
||||||
} else {
|
} else {
|
||||||
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
|
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
|
||||||
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
|
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
|
||||||
|
|
||||||
if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
|
if err := ctx.Cache.Put(cacheKey+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
|
||||||
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -685,6 +696,43 @@ func Activate(ctx *context.Context) {
|
||||||
func ActivatePost(ctx *context.Context) {
|
func ActivatePost(ctx *context.Context) {
|
||||||
code := ctx.FormString("code")
|
code := ctx.FormString("code")
|
||||||
if len(code) == 0 {
|
if len(code) == 0 {
|
||||||
|
email := ctx.FormString("email")
|
||||||
|
if len(email) > 0 {
|
||||||
|
ctx.Data["IsActivatePage"] = true
|
||||||
|
if ctx.Doer == nil || ctx.Doer.IsActive {
|
||||||
|
ctx.NotFound("invalid user", nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Change the primary email
|
||||||
|
if setting.Service.RegisterEmailConfirm {
|
||||||
|
if ctx.Cache.IsExist("MailChangeLimit_" + ctx.Doer.LowerName) {
|
||||||
|
ctx.Data["ResendLimited"] = true
|
||||||
|
} else {
|
||||||
|
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
|
||||||
|
err := user_model.ReplaceInactivePrimaryEmail(ctx, ctx.Doer.Email, &user_model.EmailAddress{
|
||||||
|
UID: ctx.Doer.ID,
|
||||||
|
Email: email,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.Data["IsActivatePage"] = false
|
||||||
|
log.Error("Couldn't replace inactive primary email of user %d: %v", ctx.Doer.ID, err)
|
||||||
|
ctx.RenderWithErr(ctx.Tr("auth.change_unconfirmed_email_error", err), TplActivate, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ctx.Cache.Put("MailChangeLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
|
||||||
|
log.Error("Set cache(MailChangeLimit) fail: %v", err)
|
||||||
|
}
|
||||||
|
if err := ctx.Cache.Put("MailChangedJustNow_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
|
||||||
|
log.Error("Set cache(MailChangedJustNow) fail: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirmation mail will be re-sent after the redirect to `/user/activate` below.
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.Data["ServiceNotEnabled"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Redirect(setting.AppSubURL + "/user/activate")
|
ctx.Redirect(setting.AppSubURL + "/user/activate")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -951,10 +951,16 @@ func SignInOAuthCallback(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
} else if !setting.Service.AllowOnlyInternalRegistration && setting.OAuth2Client.EnableAutoRegistration {
|
} else if !setting.Service.AllowOnlyInternalRegistration && setting.OAuth2Client.EnableAutoRegistration {
|
||||||
// create new user with details from oauth2 provider
|
// create new user with details from oauth2 provider
|
||||||
var missingFields []string
|
|
||||||
if gothUser.UserID == "" {
|
if gothUser.UserID == "" {
|
||||||
missingFields = append(missingFields, "sub")
|
log.Error("OAuth2 Provider %s returned empty or missing field: UserID", authSource.Name)
|
||||||
|
if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
|
||||||
|
log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("OAuth2 Provider %s returned empty or missing field: UserID", authSource.Name)
|
||||||
|
ctx.ServerError("CreateUser", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
var missingFields []string
|
||||||
if gothUser.Email == "" {
|
if gothUser.Email == "" {
|
||||||
missingFields = append(missingFields, "email")
|
missingFields = append(missingFields, "email")
|
||||||
}
|
}
|
||||||
|
@ -962,12 +968,10 @@ func SignInOAuthCallback(ctx *context.Context) {
|
||||||
missingFields = append(missingFields, "nickname")
|
missingFields = append(missingFields, "nickname")
|
||||||
}
|
}
|
||||||
if len(missingFields) > 0 {
|
if len(missingFields) > 0 {
|
||||||
log.Error("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
|
// we don't have enough information to create an account automatically,
|
||||||
if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
|
// so we prompt the user for the remaining bits
|
||||||
log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
|
log.Trace("OAuth2 Provider %s returned empty or missing fields: %s, prompting the user for them", authSource.Name, missingFields)
|
||||||
}
|
showLinkingLogin(ctx, gothUser)
|
||||||
err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
|
|
||||||
ctx.ServerError("CreateUser", err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
uname, err := getUserName(&gothUser)
|
uname, err := getUserName(&gothUser)
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/gorilla/feeds"
|
"github.com/gorilla/feeds"
|
||||||
|
"github.com/jaytaylor/html2text"
|
||||||
)
|
)
|
||||||
|
|
||||||
func toBranchLink(ctx *context.Context, act *activities_model.Action) string {
|
func toBranchLink(ctx *context.Context, act *activities_model.Action) string {
|
||||||
|
@ -240,8 +241,15 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
|
||||||
content = desc
|
content = desc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// It's a common practice for feed generators to use plain text titles.
|
||||||
|
// See https://codeberg.org/forgejo/forgejo/pulls/1595
|
||||||
|
plainTitle, err := html2text.FromString(title, html2text.Options{OmitLinks: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
items = append(items, &feeds.Item{
|
items = append(items, &feeds.Item{
|
||||||
Title: title,
|
Title: plainTitle,
|
||||||
Link: link,
|
Link: link,
|
||||||
Description: desc,
|
Description: desc,
|
||||||
Author: &feeds.Author{
|
Author: &feeds.Author{
|
||||||
|
|
|
@ -8,11 +8,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// RenderBranchFeed render format for branch or file
|
// RenderBranchFeed render format for branch or file
|
||||||
func RenderBranchFeed(ctx *context.Context) {
|
func RenderBranchFeed(feedType string) func(ctx *context.Context) {
|
||||||
_, _, showFeedType := GetFeedType(ctx.Params(":reponame"), ctx.Req)
|
return func(ctx *context.Context) {
|
||||||
if ctx.Repo.TreePath == "" {
|
if ctx.Repo.TreePath == "" {
|
||||||
ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType)
|
ShowBranchFeed(ctx, ctx.Repo.Repository, feedType)
|
||||||
} else {
|
} else {
|
||||||
ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType)
|
ShowFileFeed(ctx, ctx.Repo.Repository, feedType)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,20 @@ func View(ctx *context_module.Context) {
|
||||||
ctx.HTML(http.StatusOK, tplViewActions)
|
ctx.HTML(http.StatusOK, tplViewActions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ViewLatest(ctx *context_module.Context) {
|
||||||
|
run, err := actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.NotFound("GetLatestRun", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = run.LoadAttributes(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("LoadAttributes", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Redirect(run.HTMLURL(), http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
|
|
||||||
type ViewRequest struct {
|
type ViewRequest struct {
|
||||||
LogCursors []struct {
|
LogCursors []struct {
|
||||||
Step int `json:"step"`
|
Step int `json:"step"`
|
||||||
|
|
165
routers/web/repo/badges/badges.go
Normal file
165
routers/web/repo/badges/badges.go
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package badges
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
actions_model "code.gitea.io/gitea/models/actions"
|
||||||
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
"code.gitea.io/gitea/models/unit"
|
||||||
|
context_module "code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getBadgeURL(ctx *context_module.Context, label, text, color string) string {
|
||||||
|
sb := &strings.Builder{}
|
||||||
|
_ = setting.Badges.GeneratorURLTemplateTemplate.Execute(sb, map[string]string{
|
||||||
|
"label": url.PathEscape(label),
|
||||||
|
"text": url.PathEscape(text),
|
||||||
|
"color": url.PathEscape(color),
|
||||||
|
})
|
||||||
|
|
||||||
|
badgeURL := sb.String()
|
||||||
|
q := ctx.Req.URL.Query()
|
||||||
|
// Remove any `branch` or `event` query parameters. They're used by the
|
||||||
|
// workflow badge route, and do not need forwarding to the badge generator.
|
||||||
|
delete(q, "branch")
|
||||||
|
delete(q, "event")
|
||||||
|
if len(q) > 0 {
|
||||||
|
return fmt.Sprintf("%s?%s", badgeURL, q.Encode())
|
||||||
|
}
|
||||||
|
return badgeURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func redirectToBadge(ctx *context_module.Context, label, text, color string) {
|
||||||
|
ctx.Redirect(getBadgeURL(ctx, label, text, color))
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorBadge(ctx *context_module.Context, label, text string) {
|
||||||
|
ctx.Redirect(getBadgeURL(ctx, label, text, "crimson"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetWorkflowBadge(ctx *context_module.Context) {
|
||||||
|
branch := ctx.Req.URL.Query().Get("branch")
|
||||||
|
if branch == "" {
|
||||||
|
branch = ctx.Repo.Repository.DefaultBranch
|
||||||
|
}
|
||||||
|
branch = fmt.Sprintf("refs/heads/%s", branch)
|
||||||
|
event := ctx.Req.URL.Query().Get("event")
|
||||||
|
|
||||||
|
workflowFile := ctx.Params("workflow_name")
|
||||||
|
run, err := actions_model.GetLatestRunForBranchAndWorkflow(ctx, ctx.Repo.Repository.ID, branch, workflowFile, event)
|
||||||
|
if err != nil {
|
||||||
|
errorBadge(ctx, workflowFile, "Not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var color string
|
||||||
|
switch run.Status {
|
||||||
|
case actions_model.StatusUnknown:
|
||||||
|
color = "lightgrey"
|
||||||
|
case actions_model.StatusWaiting:
|
||||||
|
color = "lightgrey"
|
||||||
|
case actions_model.StatusRunning:
|
||||||
|
color = "gold"
|
||||||
|
case actions_model.StatusSuccess:
|
||||||
|
color = "brightgreen"
|
||||||
|
case actions_model.StatusFailure:
|
||||||
|
color = "crimson"
|
||||||
|
case actions_model.StatusCancelled:
|
||||||
|
color = "orange"
|
||||||
|
case actions_model.StatusSkipped:
|
||||||
|
color = "blue"
|
||||||
|
case actions_model.StatusBlocked:
|
||||||
|
color = "yellow"
|
||||||
|
default:
|
||||||
|
color = "lightgrey"
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectToBadge(ctx, workflowFile, run.Status.String(), color)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIssueOrPullBadge(ctx *context_module.Context, label, variant string, num int) {
|
||||||
|
var text string
|
||||||
|
if len(variant) > 0 {
|
||||||
|
text = fmt.Sprintf("%d %s", num, variant)
|
||||||
|
} else {
|
||||||
|
text = fmt.Sprintf("%d", num)
|
||||||
|
}
|
||||||
|
redirectToBadge(ctx, label, text, "blue")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIssueBadge(ctx *context_module.Context, variant string, num int) {
|
||||||
|
if !ctx.Repo.CanRead(unit.TypeIssues) &&
|
||||||
|
!ctx.Repo.CanRead(unit.TypeExternalTracker) {
|
||||||
|
errorBadge(ctx, "issues", "Not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
|
||||||
|
if err == nil {
|
||||||
|
errorBadge(ctx, "issues", "Not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getIssueOrPullBadge(ctx, "issues", variant, num)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPullBadge(ctx *context_module.Context, variant string, num int) {
|
||||||
|
if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
|
||||||
|
errorBadge(ctx, "pulls", "Not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
getIssueOrPullBadge(ctx, "pulls", variant, num)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOpenIssuesBadge(ctx *context_module.Context) {
|
||||||
|
getIssueBadge(ctx, "open", ctx.Repo.Repository.NumOpenIssues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetClosedIssuesBadge(ctx *context_module.Context) {
|
||||||
|
getIssueBadge(ctx, "closed", ctx.Repo.Repository.NumClosedIssues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTotalIssuesBadge(ctx *context_module.Context) {
|
||||||
|
getIssueBadge(ctx, "", ctx.Repo.Repository.NumIssues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOpenPullsBadge(ctx *context_module.Context) {
|
||||||
|
getPullBadge(ctx, "open", ctx.Repo.Repository.NumOpenPulls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetClosedPullsBadge(ctx *context_module.Context) {
|
||||||
|
getPullBadge(ctx, "closed", ctx.Repo.Repository.NumClosedPulls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTotalPullsBadge(ctx *context_module.Context) {
|
||||||
|
getPullBadge(ctx, "", ctx.Repo.Repository.NumPulls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetStarsBadge(ctx *context_module.Context) {
|
||||||
|
redirectToBadge(ctx, "stars", fmt.Sprintf("%d", ctx.Repo.Repository.NumStars), "blue")
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLatestReleaseBadge(ctx *context_module.Context) {
|
||||||
|
release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID)
|
||||||
|
if err != nil {
|
||||||
|
if repo_model.IsErrReleaseNotExist(err) {
|
||||||
|
errorBadge(ctx, "release", "Not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.ServerError("GetLatestReleaseByRepoID", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := release.LoadAttributes(ctx); err != nil {
|
||||||
|
ctx.ServerError("LoadAttributes", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectToBadge(ctx, "release", release.TagName, "blue")
|
||||||
|
}
|
|
@ -243,6 +243,22 @@ func FileHistory(ctx *context.Context) {
|
||||||
ctx.ServerError("CommitsByFileAndRange", err)
|
ctx.ServerError("CommitsByFileAndRange", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
oldestCommit := commits[len(commits)-1]
|
||||||
|
|
||||||
|
renamedFiles, err := git.GetCommitFileRenames(ctx, ctx.Repo.GitRepo.Path, oldestCommit.ID.String())
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetCommitFileRenames", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, renames := range renamedFiles {
|
||||||
|
if renames[1] == fileName {
|
||||||
|
ctx.Data["OldFilename"] = renames[0]
|
||||||
|
ctx.Data["OldFilenameHistory"] = fmt.Sprintf("%s/commits/commit/%s/%s", ctx.Repo.RepoLink, oldestCommit.ID.String(), renames[0])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository)
|
ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository)
|
||||||
|
|
||||||
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
ctx.Data["Username"] = ctx.Repo.Owner.Name
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/charset"
|
"code.gitea.io/gitea/modules/charset"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
@ -99,6 +100,27 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
|
||||||
return treeNames, treePaths
|
return treeNames, treePaths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getSelectableEmailAddresses returns which emails can be used by the user as
|
||||||
|
// email for a Git commiter.
|
||||||
|
func getSelectableEmailAddresses(ctx *context.Context) ([]*user_model.ActivatedEmailAddress, error) {
|
||||||
|
// Retrieve emails that the user could use for commiter identity.
|
||||||
|
commitEmails, err := user_model.GetActivatedEmailAddresses(ctx, ctx.Doer.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("GetActivatedEmailAddresses: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow for the placeholder mail to be used. Use -1 as ID to identify
|
||||||
|
// this entry to be the placerholder mail of the user.
|
||||||
|
placeholderMail := &user_model.ActivatedEmailAddress{ID: -1, Email: ctx.Doer.GetPlaceholderEmail()}
|
||||||
|
if ctx.Doer.KeepEmailPrivate {
|
||||||
|
commitEmails = append([]*user_model.ActivatedEmailAddress{placeholderMail}, commitEmails...)
|
||||||
|
} else {
|
||||||
|
commitEmails = append(commitEmails, placeholderMail)
|
||||||
|
}
|
||||||
|
|
||||||
|
return commitEmails, nil
|
||||||
|
}
|
||||||
|
|
||||||
func editFile(ctx *context.Context, isNewFile bool) {
|
func editFile(ctx *context.Context, isNewFile bool) {
|
||||||
ctx.Data["PageIsEdit"] = true
|
ctx.Data["PageIsEdit"] = true
|
||||||
ctx.Data["IsNewFile"] = isNewFile
|
ctx.Data["IsNewFile"] = isNewFile
|
||||||
|
@ -177,6 +199,12 @@ func editFile(ctx *context.Context, isNewFile bool) {
|
||||||
treeNames = append(treeNames, fileName)
|
treeNames = append(treeNames, fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commitEmails, err := getSelectableEmailAddresses(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("getSelectableEmailAddresses", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Data["TreeNames"] = treeNames
|
ctx.Data["TreeNames"] = treeNames
|
||||||
ctx.Data["TreePaths"] = treePaths
|
ctx.Data["TreePaths"] = treePaths
|
||||||
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
||||||
|
@ -192,6 +220,8 @@ func editFile(ctx *context.Context, isNewFile bool) {
|
||||||
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
|
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
|
||||||
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
||||||
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
|
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
|
||||||
|
ctx.Data["CommitMails"] = commitEmails
|
||||||
|
ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail()
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplEditFile)
|
ctx.HTML(http.StatusOK, tplEditFile)
|
||||||
}
|
}
|
||||||
|
@ -227,6 +257,12 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
|
||||||
branchName = form.NewBranchName
|
branchName = form.NewBranchName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commitEmails, err := getSelectableEmailAddresses(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("getSelectableEmailAddresses", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Data["PageIsEdit"] = true
|
ctx.Data["PageIsEdit"] = true
|
||||||
ctx.Data["PageHasPosted"] = true
|
ctx.Data["PageHasPosted"] = true
|
||||||
ctx.Data["IsNewFile"] = isNewFile
|
ctx.Data["IsNewFile"] = isNewFile
|
||||||
|
@ -243,6 +279,8 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
|
||||||
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
|
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
|
||||||
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
||||||
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath)
|
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath)
|
||||||
|
ctx.Data["CommitMails"] = commitEmails
|
||||||
|
ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail()
|
||||||
|
|
||||||
if ctx.HasError() {
|
if ctx.HasError() {
|
||||||
ctx.HTML(http.StatusOK, tplEditFile)
|
ctx.HTML(http.StatusOK, tplEditFile)
|
||||||
|
@ -277,6 +315,30 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
|
||||||
operation = "create"
|
operation = "create"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gitIdentity := &files_service.IdentityOptions{
|
||||||
|
Name: ctx.Doer.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
// -1 is defined as placeholder email.
|
||||||
|
if form.CommitMailID == -1 {
|
||||||
|
gitIdentity.Email = ctx.Doer.GetPlaceholderEmail()
|
||||||
|
} else {
|
||||||
|
// Check if the given email is activated.
|
||||||
|
email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, form.CommitMailID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetEmailAddressByID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if email == nil || !email.IsActivated {
|
||||||
|
ctx.Data["Err_CommitMailID"] = true
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_mail"), tplEditFile, &form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gitIdentity.Email = email.Email
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
|
||||||
LastCommitID: form.LastCommit,
|
LastCommitID: form.LastCommit,
|
||||||
OldBranch: ctx.Repo.BranchName,
|
OldBranch: ctx.Repo.BranchName,
|
||||||
|
@ -290,7 +352,9 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
|
||||||
ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")),
|
ContentReader: strings.NewReader(strings.ReplaceAll(form.Content, "\r", "")),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Signoff: form.Signoff,
|
Signoff: form.Signoff,
|
||||||
|
Author: gitIdentity,
|
||||||
|
Committer: gitIdentity,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
// This is where we handle all the errors thrown by files_service.ChangeRepoFiles
|
// This is where we handle all the errors thrown by files_service.ChangeRepoFiles
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
|
|
49
routers/web/repo/flags/manage.go
Normal file
49
routers/web/repo/flags/manage.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package flags
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/modules/context"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tplRepoFlags base.TplName = "repo/flags"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Manage(ctx *context.Context) {
|
||||||
|
ctx.Data["IsRepoFlagsPage"] = true
|
||||||
|
ctx.Data["Title"] = ctx.Tr("repo.admin.manage_flags")
|
||||||
|
|
||||||
|
flags := map[string]bool{}
|
||||||
|
for _, f := range setting.Repository.SettableFlags {
|
||||||
|
flags[f] = false
|
||||||
|
}
|
||||||
|
repoFlags, _ := ctx.Repo.Repository.ListFlags(ctx)
|
||||||
|
for _, f := range repoFlags {
|
||||||
|
flags[f.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Data["Flags"] = flags
|
||||||
|
|
||||||
|
ctx.HTML(http.StatusOK, tplRepoFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ManagePost(ctx *context.Context) {
|
||||||
|
newFlags := ctx.FormStrings("flags")
|
||||||
|
|
||||||
|
err := ctx.Repo.Repository.ReplaceAllFlags(ctx, newFlags)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Flash.Error(ctx.Tr("repo.admin.failed_to_replace_flags"))
|
||||||
|
log.Error("Error replacing repository flags for repo %d: %v", ctx.Repo.Repository.ID, err)
|
||||||
|
} else {
|
||||||
|
ctx.Flash.Success(ctx.Tr("repo.admin.flags_replaced"))
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.Redirect(ctx.Repo.Repository.HTMLURL() + "/flags")
|
||||||
|
}
|
|
@ -2488,7 +2488,8 @@ func UpdatePullReviewRequest(ctx *context.Context) {
|
||||||
func SearchIssues(ctx *context.Context) {
|
func SearchIssues(ctx *context.Context) {
|
||||||
before, since, err := context.GetQueryBeforeSince(ctx.Base)
|
before, since, err := context.GetQueryBeforeSince(ctx.Base)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusUnprocessableEntity, err.Error())
|
log.Error("GetQueryBeforeSince: %v", err)
|
||||||
|
ctx.Error(http.StatusUnprocessableEntity, "invalid before or since")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2525,10 +2526,11 @@ func SearchIssues(ctx *context.Context) {
|
||||||
if ctx.FormString("owner") != "" {
|
if ctx.FormString("owner") != "" {
|
||||||
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
|
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error("GetUserByName: %v", err)
|
||||||
if user_model.IsErrUserNotExist(err) {
|
if user_model.IsErrUserNotExist(err) {
|
||||||
ctx.Error(http.StatusBadRequest, "Owner not found", err.Error())
|
ctx.Error(http.StatusBadRequest, "Owner not found", err.Error())
|
||||||
} else {
|
} else {
|
||||||
ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
|
ctx.Error(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -2539,15 +2541,16 @@ func SearchIssues(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
if ctx.FormString("team") != "" {
|
if ctx.FormString("team") != "" {
|
||||||
if ctx.FormString("owner") == "" {
|
if ctx.FormString("owner") == "" {
|
||||||
ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
|
ctx.Error(http.StatusBadRequest, "Owner organisation is required for filtering on team")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
|
team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error("GetTeam: %v", err)
|
||||||
if organization.IsErrTeamNotExist(err) {
|
if organization.IsErrTeamNotExist(err) {
|
||||||
ctx.Error(http.StatusBadRequest, "Team not found", err.Error())
|
ctx.Error(http.StatusBadRequest)
|
||||||
} else {
|
} else {
|
||||||
ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
|
ctx.Error(http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -2560,7 +2563,8 @@ func SearchIssues(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
|
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error())
|
log.Error("SearchRepositoryIDs: %v", err)
|
||||||
|
ctx.Error(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(repoIDs) == 0 {
|
if len(repoIDs) == 0 {
|
||||||
|
@ -2594,7 +2598,8 @@ func SearchIssues(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
|
includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error())
|
log.Error("GetLabelIDsByNames: %v", err)
|
||||||
|
ctx.Error(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2608,7 +2613,8 @@ func SearchIssues(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
|
includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error())
|
log.Error("GetMilestoneIDsByNames: %v", err)
|
||||||
|
ctx.Error(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2675,12 +2681,14 @@ func SearchIssues(ctx *context.Context) {
|
||||||
|
|
||||||
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
|
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "SearchIssues", err.Error())
|
log.Error("SearchIssues: %v", err)
|
||||||
|
ctx.Error(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
|
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
|
log.Error("GetIssuesByIDs: %v", err)
|
||||||
|
ctx.Error(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue