Merge branch 'rebase-forgejo-dependency' into wip-forgejo

This commit is contained in:
Earl Warren 2024-02-05 18:58:23 +01:00
commit 094c84ed6d
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
292 changed files with 8842 additions and 1269 deletions

View file

@ -100,6 +100,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
@ -322,6 +324,7 @@ package "code.gitea.io/gitea/services/pull"
package "code.gitea.io/gitea/services/repository" package "code.gitea.io/gitea/services/repository"
func IsErrForkAlreadyExist func IsErrForkAlreadyExist
func UpdateRepositoryUnits
package "code.gitea.io/gitea/services/repository/archiver" package "code.gitea.io/gitea/services/repository/archiver"
func ArchiveRepository func ArchiveRepository

View file

@ -90,6 +90,8 @@ linters-settings:
desc: do not use the internal package, use AddXxx function instead desc: do not use the internal package, use AddXxx function instead
- pkg: gopkg.in/ini.v1 - pkg: gopkg.in/ini.v1
desc: do not use the ini package, use gitea's config system instead desc: do not use the ini package, use gitea's config system instead
- pkg: github.com/minio/sha256-simd
desc: use crypto/sha256 instead, see https://codeberg.org/forgejo/forgejo/pulls/1528
issues: issues:
max-issues-per-linter: 0 max-issues-per-linter: 0

File diff suppressed because one or more lines are too long

View file

@ -70,7 +70,7 @@ func runGenerateInternalToken(c *cli.Context) error {
} }
func runGenerateLfsJwtSecret(c *cli.Context) error { func runGenerateLfsJwtSecret(c *cli.Context) error {
_, jwtSecretBase64, err := generate.NewJwtSecretBase64() _, jwtSecretBase64, err := generate.NewJwtSecret()
if err != nil { if err != nil {
return err return err
} }

View file

@ -1,4 +1,4 @@
{ {
"go.buildTags": "'sqlite sqlite_unlock_notify'", "go.buildTags": "sqlite,sqlite_unlock_notify",
"go.testFlags": ["-v"] "go.testFlags": ["-v"]
} }

View file

@ -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]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1470,6 +1487,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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1783,9 +1802,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

View file

@ -458,6 +458,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_THRESHOLD` **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.
@ -517,6 +518,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
View file

@ -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-rc6 github.com/opencontainers/image-spec v1.1.0-rc6
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.18.0 github.com/prometheus/client_golang v1.18.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.27.1 github.com/urfave/cli/v2 v2.27.1
github.com/xanzy/go-gitlab v0.96.0 github.com/xanzy/go-gitlab v0.96.0
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
@ -230,6 +226,7 @@ require (
github.com/mholt/acmez v1.2.0 // indirect github.com/mholt/acmez v1.2.0 // indirect
github.com/miekg/dns v1.1.58 // indirect github.com/miekg/dns v1.1.58 // 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
@ -245,6 +242,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.21 // indirect github.com/pierrec/lz4/v4 v4.1.21 // 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.46.0 // indirect github.com/prometheus/common v0.46.0 // indirect
@ -275,8 +273,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
View file

@ -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 v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
@ -832,13 +830,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=

View file

@ -171,13 +171,12 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err
} }
// CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow. // CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow.
func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error { func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string) error {
// Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'. // Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'.
runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{ runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
RepoID: repoID, RepoID: repoID,
Ref: ref, Ref: ref,
WorkflowID: workflowID, WorkflowID: workflowID,
TriggerEvent: event,
Status: []Status{StatusRunning, StatusWaiting}, Status: []Status{StatusRunning, StatusWaiting},
}) })
if err != nil { if err != nil {
@ -312,6 +311,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)

View file

@ -10,7 +10,6 @@ import (
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
webhook_module "code.gitea.io/gitea/modules/webhook"
"xorm.io/builder" "xorm.io/builder"
) )
@ -72,7 +71,6 @@ type FindRunOptions struct {
WorkflowID string WorkflowID string
Ref string // the commit/tag/… that caused this workflow Ref string // the commit/tag/… that caused this workflow
TriggerUserID int64 TriggerUserID int64
TriggerEvent webhook_module.HookEventType
Approved bool // not util.OptionalBool, it works only when it's true Approved bool // not util.OptionalBool, it works only when it's true
Status []Status Status []Status
} }
@ -100,9 +98,6 @@ func (opts FindRunOptions) ToConds() builder.Cond {
if opts.Ref != "" { if opts.Ref != "" {
cond = cond.And(builder.Eq{"ref": opts.Ref}) cond = cond.And(builder.Eq{"ref": opts.Ref})
} }
if opts.TriggerEvent != "" {
cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent})
}
return cond return cond
} }

View file

@ -5,7 +5,6 @@ package actions
import ( import (
"context" "context"
"fmt"
"time" "time"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -119,22 +118,3 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error {
return committer.Commit() return committer.Commit()
} }
func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error {
// If actions disabled when there is schedule task, this will remove the outdated schedule tasks
// There is no other place we can do this because the app.ini will be changed manually
if err := DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
return fmt.Errorf("DeleteCronTaskByRepo: %v", err)
}
// cancel running cron jobs of this repository and delete old schedules
if err := CancelRunningJobs(
ctx,
repo.ID,
repo.DefaultBranch,
"",
webhook_module.HookEventSchedule,
); err != nil {
return fmt.Errorf("CancelRunningJobs: %v", err)
}
return nil
}

View file

@ -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",

View file

@ -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)
} }

View file

@ -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)
} }

View file

@ -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) {

View 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)
})
}

View file

@ -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 {

View file

@ -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
View 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)
})
}

View file

@ -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"
) )

View file

@ -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.SlowQueryThreshold > 0 {
xormEngine.AddHook(&SlowQueryHook{
Treshold: setting.Database.SlowQueryThreshold,
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
}

View file

@ -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", log.INFO)
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", log.INFO)
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)
}

View file

@ -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

View file

@ -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: 1059
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

View file

@ -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: 1059
type: 1
created_unix: 946684810
-
id: 1094
repo_id: 1059
type: 2
created_unix: 946684810
-
id: 1095
repo_id: 1059
type: 3
created_unix: 946684810
-
id: 1096
repo_id: 1059
type: 4
created_unix: 946684810
-
id: 1097
repo_id: 1059
type: 5
created_unix: 946684810
# END Forgejo [GITEA] Improve HTML title on repositories
- -
id: 91 id: 91
repo_id: 58 repo_id: 58

View file

@ -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
@ -1694,6 +1695,19 @@
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: 1059
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
- -
id: 59 id: 59
owner_id: 2 owner_id: 2

View file

@ -66,7 +66,7 @@
num_followers: 2 num_followers: 2
num_following: 1 num_following: 1
num_stars: 2 num_stars: 2
num_repos: 15 num_repos: 16
num_teams: 0 num_teams: 0
num_members: 0 num_members: 0
visibility: 0 visibility: 0

View file

@ -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,12 @@ 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),
// v5 -> v6
NewMigration("Add wiki_branch to repository", forgejo_v1_22.AddWikiBranchToRepository),
} }
// GetCurrentDBVersion returns the current Forgejo database version. // GetCurrentDBVersion returns the current Forgejo database version.

View 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{})
}

View 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))
}

View file

@ -0,0 +1,24 @@
// 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"
)
func AddWikiBranchToRepository(x *xorm.Engine) error {
type Repository struct {
ID int64
WikiBranch string
}
if err := x.Sync(&Repository{}); err != nil {
return err
}
// Update existing repositories to use `master` as the wiki branch, for
// compatilibty's sake.
_, err := x.Exec("UPDATE repository SET wiki_branch = 'master' WHERE wiki_branch = '' OR wiki_branch IS NULL")
return err
}

View file

@ -128,6 +128,10 @@ func (b *Branch) LoadDeletedBy(ctx context.Context) (err error) {
return err return err
} }
func (b *Branch) GetRepo(ctx context.Context) (*repo_model.Repository, error) {
return repo_model.GetRepositoryByID(ctx, b.RepoID)
}
func (b *Branch) LoadPusher(ctx context.Context) (err error) { func (b *Branch) LoadPusher(ctx context.Context) (err error) {
if b.Pusher == nil && b.PusherID > 0 { if b.Pusher == nil && b.PusherID > 0 {
b.Pusher, err = user_model.GetUserByID(ctx, b.PusherID) b.Pusher, err = user_model.GetUserByID(ctx, b.PusherID)
@ -283,7 +287,7 @@ func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *
} }
// RenameBranch rename a branch // RenameBranch rename a branch
func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(ctx context.Context, isDefault bool) error) (err error) { func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(isDefault bool) error) (err error) {
ctx, committer, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return err return err
@ -358,7 +362,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
} }
// 5. do git action // 5. do git action
if err = gitAction(ctx, isDefault); err != nil { if err = gitAction(isDefault); err != nil {
return err return err
} }

View file

@ -4,7 +4,6 @@
package git_test package git_test
import ( import (
"context"
"testing" "testing"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -133,7 +132,7 @@ func TestRenameBranch(t *testing.T) {
}, git_model.WhitelistOptions{})) }, git_model.WhitelistOptions{}))
assert.NoError(t, committer.Commit()) assert.NoError(t, committer.Commit())
assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(ctx context.Context, isDefault bool) error { assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(isDefault bool) error {
_isDefault = isDefault _isDefault = isDefault
return nil return nil
})) }))

View file

@ -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)
}

View file

@ -9,6 +9,14 @@ import (
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
) )
func GetMaxIssueIndexForRepo(ctx context.Context, repoID int64) (int64, error) {
var max int64
if _, err := db.GetEngine(ctx).Select("MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil {
return 0, err
}
return max, nil
}
// RecalculateIssueIndexForRepo create issue_index for repo if not exist and // RecalculateIssueIndexForRepo create issue_index for repo if not exist and
// update it based on highest index of existing issues assigned to a repo // update it based on highest index of existing issues assigned to a repo
func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error { func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error {
@ -18,8 +26,8 @@ func RecalculateIssueIndexForRepo(ctx context.Context, repoID int64) error {
} }
defer committer.Close() defer committer.Close()
var max int64 max, err := GetMaxIssueIndexForRepo(ctx, repoID)
if _, err = db.GetEngine(ctx).Select(" MAX(`index`)").Table("issue").Where("repo_id=?", repoID).Get(&max); err != nil { if err != nil {
return err return err
} }

View file

@ -0,0 +1,38 @@
// Copyright 2024 The Forgejo Authors
// SPDX-License-Identifier: MIT
package issues_test
import (
"testing"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
"github.com/stretchr/testify/assert"
)
func TestGetMaxIssueIndexForRepo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
maxPR, err := issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repo.ID)
assert.NoError(t, err)
issue := testCreateIssue(t, repo.ID, repo.OwnerID, "title1", "content1", false)
assert.Greater(t, issue.Index, maxPR)
maxPR, err = issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repo.ID)
assert.NoError(t, err)
pull := testCreateIssue(t, repo.ID, repo.OwnerID, "title2", "content2", true)
assert.Greater(t, pull.Index, maxPR)
maxPR, err = issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repo.ID)
assert.NoError(t, err)
assert.Equal(t, maxPR, pull.Index)
}

View file

@ -50,6 +50,14 @@ func listPullRequestStatement(ctx context.Context, baseRepoID int64, opts *PullR
return sess, nil return sess, nil
} }
func GetUnmergedPullRequestsByHeadInfoMax(ctx context.Context, repoID, maxIndex int64, branch string) ([]*PullRequest, error) {
prs := make([]*PullRequest, 0, 2)
sess := db.GetEngine(ctx).
Join("INNER", "issue", "issue.id = `pull_request`.issue_id").
Where("`pull_request`.head_repo_id = ? AND `pull_request`.head_branch = ? AND `pull_request`.has_merged = ? AND `issue`.is_closed = ? AND `pull_request`.flow = ? AND `issue`.`index` <= ?", repoID, branch, false, false, PullRequestFlowGithub, maxIndex)
return prs, sess.Find(&prs)
}
// GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged // GetUnmergedPullRequestsByHeadInfo returns all pull requests that are open and has not been merged
func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) { func GetUnmergedPullRequestsByHeadInfo(ctx context.Context, repoID int64, branch string) ([]*PullRequest, error) {
prs := make([]*PullRequest, 0, 2) prs := make([]*PullRequest, 0, 2)

View file

@ -4,6 +4,7 @@
package issues_test package issues_test
import ( import (
"fmt"
"testing" "testing"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
@ -158,6 +159,91 @@ func TestGetUnmergedPullRequestsByHeadInfo(t *testing.T) {
} }
} }
func TestGetUnmergedPullRequestsByHeadInfoMax(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repoID := int64(1)
maxPR := int64(0)
prs, err := issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, maxPR, "branch2")
assert.NoError(t, err)
assert.Len(t, prs, 0)
maxPR, err = issues_model.GetMaxIssueIndexForRepo(db.DefaultContext, repoID)
assert.NoError(t, err)
prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, maxPR, "branch2")
assert.NoError(t, err)
assert.Len(t, prs, 1)
for _, pr := range prs {
assert.Equal(t, int64(1), pr.HeadRepoID)
assert.Equal(t, "branch2", pr.HeadBranch)
}
pr := prs[0]
for _, testCase := range []struct {
table string
field string
id int64
match any
nomatch any
}{
{
table: "issue",
field: "is_closed",
id: pr.IssueID,
match: false,
nomatch: true,
},
{
table: "pull_request",
field: "flow",
id: pr.ID,
match: issues_model.PullRequestFlowGithub,
nomatch: issues_model.PullRequestFlowAGit,
},
{
table: "pull_request",
field: "head_repo_id",
id: pr.ID,
match: pr.HeadRepoID,
nomatch: 0,
},
{
table: "pull_request",
field: "head_branch",
id: pr.ID,
match: pr.HeadBranch,
nomatch: "something else",
},
{
table: "pull_request",
field: "has_merged",
id: pr.ID,
match: false,
nomatch: true,
},
} {
t.Run(testCase.field, func(t *testing.T) {
update := fmt.Sprintf("UPDATE `%s` SET `%s` = ? WHERE `id` = ?", testCase.table, testCase.field)
// expect no match
_, err = db.GetEngine(db.DefaultContext).Exec(update, testCase.nomatch, testCase.id)
assert.NoError(t, err)
prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, maxPR, "branch2")
assert.NoError(t, err)
assert.Len(t, prs, 0)
// expect one match
_, err = db.GetEngine(db.DefaultContext).Exec(update, testCase.match, testCase.id)
assert.NoError(t, err)
prs, err = issues_model.GetUnmergedPullRequestsByHeadInfoMax(db.DefaultContext, repoID, maxPR, "branch2")
assert.NoError(t, err)
assert.Len(t, prs, 1)
// identical to the known PR
assert.Equal(t, pr.ID, prs[0].ID)
})
}
}
func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) { func TestGetUnmergedPullRequestsByBaseInfo(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(db.DefaultContext, 1, "master") prs, err := issues_model.GetUnmergedPullRequestsByBaseInfo(db.DefaultContext, 1, "master")

View file

@ -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"
) )

View file

@ -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"

View file

@ -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
}

View file

@ -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)
} }
} }
} }

View file

@ -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
} }

View file

@ -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"
@ -132,3 +138,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
}

View file

@ -135,6 +135,7 @@ type Repository struct {
OriginalServiceType api.GitServiceType `xorm:"index"` OriginalServiceType api.GitServiceType `xorm:"index"`
OriginalURL string `xorm:"VARCHAR(2048)"` OriginalURL string `xorm:"VARCHAR(2048)"`
DefaultBranch string DefaultBranch string
WikiBranch string
NumWatches int NumWatches int
NumStars int NumStars int
@ -204,6 +205,13 @@ func (repo *Repository) GetOwnerName() string {
return repo.OwnerName return repo.OwnerName
} }
func (repo *Repository) GetWikiBranchName() string {
if repo.WikiBranch == "" {
return setting.Repository.DefaultBranch
}
return repo.WikiBranch
}
// SanitizedOriginalURL returns a sanitized OriginalURL // SanitizedOriginalURL returns a sanitized OriginalURL
func (repo *Repository) SanitizedOriginalURL() string { func (repo *Repository) SanitizedOriginalURL() string {
if repo.OriginalURL == "" { if repo.OriginalURL == "" {

102
models/repo/repo_flags.go Normal file
View 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()
}

View 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)
}

View file

@ -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",

View file

@ -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,6 +40,35 @@ 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
@ -46,6 +76,7 @@ type RepoUnit struct { //revive:disable-line:exported
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() {
@ -283,3 +314,29 @@ func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error {
_, err := db.GetEngine(ctx).ID(unit.ID).Update(unit) _, err := db.GetEngine(ctx).ID(unit.ID).Update(unit)
return err return err
} }
// UpdateRepositoryUnits updates a repository's units
func UpdateRepositoryUnits(ctx context.Context, repo *Repository, units []RepoUnit, deleteUnitTypes []unit.Type) (err error) {
ctx, committer, err := db.TxContext(ctx)
if err != nil {
return err
}
defer committer.Close()
// Delete existing settings of units before adding again
for _, u := range units {
deleteUnitTypes = append(deleteUnitTypes, u.Type)
}
if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(RepoUnit)); err != nil {
return err
}
if len(units) > 0 {
if err = db.Insert(ctx, units); err != nil {
return err
}
}
return committer.Commit()
}

View file

@ -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)
}

View file

@ -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)

View 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.NewReplacer("/", "_", "?", "!").Replace(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)
}

View file

@ -231,6 +231,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
@ -313,31 +332,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 MakeEmailPrimaryWithUser(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
@ -367,6 +362,30 @@ func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
return committer.Commit() return committer.Commit()
} }
// 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 MakeEmailPrimaryWithUser(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

View file

@ -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"
@ -219,3 +220,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)
})
}
}

View file

@ -216,6 +216,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

View file

@ -484,6 +484,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() {
@ -501,6 +511,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

View file

@ -35,6 +35,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
case GithubEventGollum: case GithubEventGollum:
return triggedEvent == webhook_module.HookEventWiki return triggedEvent == webhook_module.HookEventWiki
case GithubEventSchedule:
return triggedEvent == webhook_module.HookEventSchedule
case GithubEventIssues: case GithubEventIssues:
switch triggedEvent { switch triggedEvent {
case webhook_module.HookEventIssues, case webhook_module.HookEventIssues,
@ -70,9 +73,6 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
return false return false
} }
case GithubEventSchedule:
return triggedEvent == webhook_module.HookEventSchedule
default: default:
return eventName == string(triggedEvent) return eventName == string(triggedEvent)
} }

View file

@ -22,7 +22,7 @@ import (
type DetectedWorkflow struct { type DetectedWorkflow struct {
EntryName string EntryName string
TriggerEvent *jobparser.Event TriggerEvent string
Content []byte Content []byte
} }
@ -103,7 +103,6 @@ func DetectWorkflows(
commit *git.Commit, commit *git.Commit,
triggedEvent webhook_module.HookEventType, triggedEvent webhook_module.HookEventType,
payload api.Payloader, payload api.Payloader,
detectSchedule bool,
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) { ) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
entries, err := ListWorkflows(commit) entries, err := ListWorkflows(commit)
if err != nil { if err != nil {
@ -118,7 +117,6 @@ func DetectWorkflows(
return nil, nil, err return nil, nil, err
} }
// one workflow may have multiple events
events, err := GetEventsFromContent(content) events, err := GetEventsFromContent(content)
if err != nil { if err != nil {
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err) log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
@ -127,18 +125,17 @@ func DetectWorkflows(
for _, evt := range events { for _, evt := range events {
log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent) log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent)
if evt.IsSchedule() { if evt.IsSchedule() {
if detectSchedule {
dwf := &DetectedWorkflow{ dwf := &DetectedWorkflow{
EntryName: entry.Name(), EntryName: entry.Name(),
TriggerEvent: evt, TriggerEvent: evt.Name,
Content: content, Content: content,
} }
schedules = append(schedules, dwf) schedules = append(schedules, dwf)
} }
} else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) { if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
dwf := &DetectedWorkflow{ dwf := &DetectedWorkflow{
EntryName: entry.Name(), EntryName: entry.Name(),
TriggerEvent: evt, TriggerEvent: evt.Name,
Content: content, Content: content,
} }
workflows = append(workflows, dwf) workflows = append(workflows, dwf)
@ -149,41 +146,6 @@ func DetectWorkflows(
return workflows, schedules, nil return workflows, schedules, nil
} }
func DetectScheduledWorkflows(gitRepo *git.Repository, commit *git.Commit) ([]*DetectedWorkflow, error) {
entries, err := ListWorkflows(commit)
if err != nil {
return nil, err
}
wfs := make([]*DetectedWorkflow, 0, len(entries))
for _, entry := range entries {
content, err := GetContentFromEntry(entry)
if err != nil {
return nil, err
}
// one workflow may have multiple events
events, err := GetEventsFromContent(content)
if err != nil {
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
continue
}
for _, evt := range events {
if evt.IsSchedule() {
log.Trace("detect scheduled workflow: %q", entry.Name())
dwf := &DetectedWorkflow{
EntryName: entry.Name(),
TriggerEvent: evt,
Content: content,
}
wfs = append(wfs, dwf)
}
}
}
return wfs, nil
}
func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool { func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent webhook_module.HookEventType, payload api.Payloader, evt *jobparser.Event) bool {
if !canGithubEventMatch(evt.Name, triggedEvent) { if !canGithubEventMatch(evt.Name, triggedEvent) {
return false return false
@ -191,11 +153,11 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
switch triggedEvent { switch triggedEvent {
case // events with no activity types case // events with no activity types
webhook_module.HookEventSchedule,
webhook_module.HookEventCreate, webhook_module.HookEventCreate,
webhook_module.HookEventDelete, webhook_module.HookEventDelete,
webhook_module.HookEventFork, webhook_module.HookEventFork,
webhook_module.HookEventWiki, webhook_module.HookEventWiki:
webhook_module.HookEventSchedule:
if len(evt.Acts()) != 0 { if len(evt.Acts()) != 0 {
log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts()) log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts())
} }

View file

@ -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"
) )

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -11,6 +11,7 @@ import (
"net/url" "net/url"
"strings" "strings"
issues_model "code.gitea.io/gitea/models/issues"
"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"
mc "code.gitea.io/gitea/modules/cache" mc "code.gitea.io/gitea/modules/cache"
@ -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
} }

View file

@ -400,6 +400,7 @@ func repoAssignment(ctx *Context, repo *repo_model.Repository) {
ctx.Data["PushMirrors"] = pushMirrors ctx.Data["PushMirrors"] = pushMirrors
ctx.Data["RepoName"] = ctx.Repo.Repository.Name ctx.Data["RepoName"] = ctx.Repo.Repository.Name
ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty ctx.Data["IsEmptyRepo"] = ctx.Repo.Repository.IsEmpty
ctx.Data["DefaultWikiBranchName"] = setting.Repository.DefaultBranch
} }
// RepoIDAssignment returns a handler which assigns the repo to the context. // RepoIDAssignment returns a handler which assigns the repo to the context.

View file

@ -38,22 +38,14 @@ func NewInternalToken() (string, error) {
return internalToken, nil return internalToken, nil
} }
// NewJwtSecret generates a new value intended to be used for JWT secrets. // NewJwtSecret generates a new base64 encoded value intended to be used for JWT secrets.
func NewJwtSecret() ([]byte, error) { func NewJwtSecret() ([]byte, string, error) {
bytes := make([]byte, 32) bytes := make([]byte, 32)
_, err := io.ReadFull(rand.Reader, bytes) _, err := rand.Read(bytes)
if err != nil {
return nil, err
}
return bytes, nil
}
// NewJwtSecretBase64 generates a new base64 encoded value intended to be used for JWT secrets.
func NewJwtSecretBase64() ([]byte, string, error) {
bytes, err := NewJwtSecret()
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
return bytes, base64.RawURLEncoding.EncodeToString(bytes), nil return bytes, base64.RawURLEncoding.EncodeToString(bytes), nil
} }

View file

@ -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})

View file

@ -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)
}
}

View file

@ -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

View file

@ -291,7 +291,7 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe
} }
checker := &CheckAttributeReader{ checker := &CheckAttributeReader{
Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"}, Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"},
Repo: repo, Repo: repo,
IndexFile: indexFilename, IndexFile: indexFilename,
WorkTree: worktree, WorkTree: worktree,

View file

@ -13,6 +13,18 @@ const (
bigFileSize int64 = 1024 * 1024 // 1 MiB bigFileSize int64 = 1024 * 1024 // 1 MiB
) )
type LinguistBoolAttrib struct {
Value string
}
func (attrib *LinguistBoolAttrib) IsTrue() bool {
return attrib.Value == "set" || attrib.Value == "true"
}
func (attrib *LinguistBoolAttrib) IsFalse() bool {
return attrib.Value == "unset" || attrib.Value == "false"
}
// mergeLanguageStats mergers language names with different cases. The name with most upper case letters is used. // mergeLanguageStats mergers language names with different cases. The name with most upper case letters is used.
func mergeLanguageStats(stats map[string]int64) map[string]int64 { func mergeLanguageStats(stats map[string]int64) map[string]int64 {
names := map[string]struct { names := map[string]struct {

View file

@ -1,4 +1,5 @@
// Copyright 2020 The Gitea Authors. All rights reserved. // Copyright 2020 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
//go:build gogit //go:build gogit
@ -57,23 +58,25 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
return nil return nil
} }
notVendored := false isVendored := LinguistBoolAttrib{}
notGenerated := false isGenerated := LinguistBoolAttrib{}
isDocumentation := LinguistBoolAttrib{}
isDetectable := LinguistBoolAttrib{}
if checker != nil { if checker != nil {
attrs, err := checker.CheckPath(f.Name) attrs, err := checker.CheckPath(f.Name)
if err == nil { if err == nil {
if vendored, has := attrs["linguist-vendored"]; has { if vendored, has := attrs["linguist-vendored"]; has {
if vendored == "set" || vendored == "true" { isVendored = LinguistBoolAttrib{Value: vendored}
return nil
}
notVendored = vendored == "false"
} }
if generated, has := attrs["linguist-generated"]; has { if generated, has := attrs["linguist-generated"]; has {
if generated == "set" || generated == "true" { isGenerated = LinguistBoolAttrib{Value: generated}
return nil
} }
notGenerated = generated == "false" if documentation, has := attrs["linguist-documentation"]; has {
isDocumentation = LinguistBoolAttrib{Value: documentation}
}
if detectable, has := attrs["linguist-detectable"]; has {
isDetectable = LinguistBoolAttrib{Value: detectable}
} }
if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
// group languages, such as Pug -> HTML; SCSS -> CSS // group languages, such as Pug -> HTML; SCSS -> CSS
@ -105,8 +108,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
} }
} }
if (!notVendored && analyze.IsVendor(f.Name)) || enry.IsDotFile(f.Name) || if isDetectable.IsFalse() || isVendored.IsTrue() || isDocumentation.IsTrue() ||
enry.IsDocumentation(f.Name) || enry.IsConfiguration(f.Name) { (!isVendored.IsFalse() && analyze.IsVendor(f.Name)) ||
enry.IsDotFile(f.Name) ||
enry.IsConfiguration(f.Name) ||
(!isDocumentation.IsFalse() && enry.IsDocumentation(f.Name)) {
return nil return nil
} }
@ -115,12 +121,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
if f.Size <= bigFileSize { if f.Size <= bigFileSize {
content, _ = readFile(f, fileSizeLimit) content, _ = readFile(f, fileSizeLimit)
} }
if !notGenerated && enry.IsGenerated(f.Name, content) { if !isGenerated.IsTrue() && enry.IsGenerated(f.Name, content) {
return nil return nil
} }
// TODO: Use .gitattributes file for linguist overrides // TODO: Use .gitattributes file for linguist overrides
language := analyze.GetCodeLanguage(f.Name, content) language := analyze.GetCodeLanguage(f.Name, content)
if language == enry.OtherLanguage || language == "" { if language == enry.OtherLanguage || language == "" {
return nil return nil
@ -136,6 +141,13 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
if !checked { if !checked {
langtype := enry.GetLanguageType(language) langtype := enry.GetLanguageType(language)
included = langtype == enry.Programming || langtype == enry.Markup included = langtype == enry.Programming || langtype == enry.Markup
if !included {
if isDetectable.IsTrue() {
included = true
} else {
return nil
}
}
includedLanguage[language] = included includedLanguage[language] = included
} }
if included { if included {

View file

@ -1,4 +1,5 @@
// Copyright 2020 The Gitea Authors. All rights reserved. // Copyright 2020 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
//go:build !gogit //go:build !gogit
@ -90,23 +91,25 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
continue continue
} }
notVendored := false isVendored := LinguistBoolAttrib{}
notGenerated := false isGenerated := LinguistBoolAttrib{}
isDocumentation := LinguistBoolAttrib{}
isDetectable := LinguistBoolAttrib{}
if checker != nil { if checker != nil {
attrs, err := checker.CheckPath(f.Name()) attrs, err := checker.CheckPath(f.Name())
if err == nil { if err == nil {
if vendored, has := attrs["linguist-vendored"]; has { if vendored, has := attrs["linguist-vendored"]; has {
if vendored == "set" || vendored == "true" { isVendored = LinguistBoolAttrib{Value: vendored}
continue
}
notVendored = vendored == "false"
} }
if generated, has := attrs["linguist-generated"]; has { if generated, has := attrs["linguist-generated"]; has {
if generated == "set" || generated == "true" { isGenerated = LinguistBoolAttrib{Value: generated}
continue
} }
notGenerated = generated == "false" if documentation, has := attrs["linguist-documentation"]; has {
isDocumentation = LinguistBoolAttrib{Value: documentation}
}
if detectable, has := attrs["linguist-detectable"]; has {
isDetectable = LinguistBoolAttrib{Value: detectable}
} }
if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" { if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
// group languages, such as Pug -> HTML; SCSS -> CSS // group languages, such as Pug -> HTML; SCSS -> CSS
@ -139,8 +142,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
} }
} }
if (!notVendored && analyze.IsVendor(f.Name())) || enry.IsDotFile(f.Name()) || if isDetectable.IsFalse() || isVendored.IsTrue() || isDocumentation.IsTrue() ||
enry.IsDocumentation(f.Name()) || enry.IsConfiguration(f.Name()) { (!isVendored.IsFalse() && analyze.IsVendor(f.Name())) ||
enry.IsDotFile(f.Name()) ||
enry.IsConfiguration(f.Name()) ||
(!isDocumentation.IsFalse() && enry.IsDocumentation(f.Name())) {
continue continue
} }
@ -173,7 +179,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
return nil, err return nil, err
} }
} }
if !notGenerated && enry.IsGenerated(f.Name(), content) { if !isGenerated.IsTrue() && enry.IsGenerated(f.Name(), content) {
continue continue
} }
@ -194,6 +200,13 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
if !checked { if !checked {
langType := enry.GetLanguageType(language) langType := enry.GetLanguageType(language)
included = langType == enry.Programming || langType == enry.Markup included = langType == enry.Programming || langType == enry.Markup
if !included {
if isDetectable.IsTrue() {
included = true
} else {
continue
}
}
includedLanguage[language] = included includedLanguage[language] = included
} }
if included { if included {

View file

@ -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}
}

View file

@ -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 (

View file

@ -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 (

View file

@ -29,13 +29,18 @@ 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 {
result = append(result, unicode.ToLower(r)) case unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_':
} if needsDash && len(result) > 0 {
if unicode.IsSpace(r) {
result = append(result, '-') result = append(result, '-')
} }
needsDash = false
result = append(result, unicode.ToLower(r))
default:
needsDash = true
}
} }
return []byte(string(result)) return []byte(string(result))
} }

View file

@ -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"},
{"test4", "test4"}, {"test*3", "test-3"},
{"test5", "test5"}, {"test4", "test-4"},
{"test*6", "test6"}, {"test5", "test-5"},
{"test6 a", "test6-a"}, {"test*6", "test-6"},
{"test6 !b", "test6-b"}, {"test6 a", "test-6-a"},
{"testad # df", "testad--df"}, {"test6 !b", "test-6-b"},
{"testad #23 df 2*/*", "testad-23-df-2"}, {"testad # df", "test-ad-df"},
{"testad 23 df 2*/*", "testad-23-df-2"}, {"testad #23 df 2*/*", "test-ad-23-df-2"},
{"testad # 23 df 2*/*", "testad--23-df-2"}, {"testad 23 df 2*/*", "test-ad-23-df-2"},
{"testad # 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"},
{"test6a", "test6a"}, {"test6a", "test-6a"},
{"testa6", "testa6"}, {"testa6", "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"},
{"tes0", "tes0"}, {"tes0", "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)

View file

@ -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

View file

@ -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.

View file

@ -135,7 +135,12 @@ type Writer struct {
const mailto = "mailto:" const mailto = "mailto:"
func (r *Writer) resolveLink(l org.RegularLink) string { func (r *Writer) resolveLink(node org.Node) string {
l, ok := node.(org.RegularLink)
if !ok {
l = org.RegularLink{URL: strings.TrimPrefix(org.String(node), "file:")}
}
link := html.EscapeString(l.URL) link := html.EscapeString(l.URL)
if l.Protocol == "file" { if l.Protocol == "file" {
link = link[len("file:"):] link = link[len("file:"):]
@ -162,14 +167,14 @@ func (r *Writer) WriteRegularLink(l org.RegularLink) {
if l.Description == nil { if l.Description == nil {
fmt.Fprintf(r, `<img src="%s" alt="%s" />`, link, link) fmt.Fprintf(r, `<img src="%s" alt="%s" />`, link, link)
} else { } else {
imageSrc := r.resolveLink(l.Description[0].(org.RegularLink)) imageSrc := r.resolveLink(l.Description[0])
fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc) fmt.Fprintf(r, `<a href="%s"><img src="%s" alt="%s" /></a>`, link, imageSrc, imageSrc)
} }
case "video": case "video":
if l.Description == nil { if l.Description == nil {
fmt.Fprintf(r, `<video src="%s">%s</video>`, link, link) fmt.Fprintf(r, `<video src="%s">%s</video>`, link, link)
} else { } else {
videoSrc := r.resolveLink(l.Description[0].(org.RegularLink)) videoSrc := r.resolveLink(l.Description[0])
fmt.Fprintf(r, `<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc) fmt.Fprintf(r, `<a href="%s"><video src="%s">%s</video></a>`, link, videoSrc, videoSrc)
} }
default: default:

View file

@ -80,6 +80,12 @@ func TestRender_Media(t *testing.T) {
`<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></p>`) `<p><img src="https://example.com/example.svg" alt="https://example.com/example.svg" /></p>`)
test("[[https://example.com/example.mp4]]", test("[[https://example.com/example.mp4]]",
`<p><video src="https://example.com/example.mp4">https://example.com/example.mp4</video></p>`) `<p><video src="https://example.com/example.mp4">https://example.com/example.mp4</video></p>`)
// Text description.
test("[[file:./lem-post.png][file:./lem-post.png]]",
`<p><a href="http://localhost:3000/gogits/gogs/lem-post.png"><img src="http://localhost:3000/gogits/gogs/lem-post.png" alt="http://localhost:3000/gogits/gogs/lem-post.png" /></a></p>`)
test("[[file:./lem-post.mp4][file:./lem-post.mp4]]",
`<p><a href="http://localhost:3000/gogits/gogs/lem-post.mp4"><video src="http://localhost:3000/gogits/gogs/lem-post.mp4">http://localhost:3000/gogits/gogs/lem-post.mp4</video></a></p>`)
} }
func TestRender_Source(t *testing.T) { func TestRender_Source(t *testing.T) {

View file

@ -1,4 +1,5 @@
// 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 repository package repository
@ -99,7 +100,6 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
Mirror: true, Mirror: true,
Quiet: true, Quiet: true,
Timeout: migrateTimeout, Timeout: migrateTimeout,
Branch: "master",
SkipTLSVerify: setting.Migrations.SkipTLSVerify, SkipTLSVerify: setting.Migrations.SkipTLSVerify,
}); err != nil { }); err != nil {
log.Warn("Clone wiki: %v", err) log.Warn("Clone wiki: %v", err)
@ -107,6 +107,30 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err) return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
} }
} else { } else {
// Figure out the branch of the wiki we just cloned. We assume
// that the default branch is to be used, and we'll use the same
// name as the source.
gitRepo, err := git.OpenRepository(ctx, wikiPath)
if err != nil {
log.Warn("Failed to open wiki repository during migration: %v", err)
if err := util.RemoveAll(wikiPath); err != nil {
return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
}
return repo, err
}
defer gitRepo.Close()
branch, err := gitRepo.GetDefaultBranch()
if err != nil {
log.Warn("Failed to get the default branch of a migrated wiki repo: %v", err)
if err := util.RemoveAll(wikiPath); err != nil {
return repo, fmt.Errorf("Failed to remove %s: %w", wikiPath, err)
}
return repo, err
}
repo.WikiBranch = branch
if err := git.WriteCommitGraph(ctx, wikiPath); err != nil { if err := git.WriteCommitGraph(ctx, wikiPath); err != nil {
return repo, err return repo, err
} }

View file

@ -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.

View file

@ -7,6 +7,7 @@ package setting
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
View 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))
}

View file

@ -45,6 +45,7 @@ var (
ConnMaxLifetime time.Duration ConnMaxLifetime time.Duration
IterateBufferSize int IterateBufferSize int
AutoMigration bool AutoMigration bool
SlowQueryThreshold time.Duration
}{ }{
Timeout: 500, Timeout: 500,
IterateBufferSize: 50, IterateBufferSize: 50,
@ -87,6 +88,13 @@ 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)
deprecatedSetting(rootCfg, "database", "SLOW_QUERY_TRESHOLD", "database", "SLOW_QUERY_THRESHOLD", "1.23")
if sec.HasKey("SLOW_QUERY_TRESHOLD") && !sec.HasKey("SLOW_QUERY_THRESHOLD") {
Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_TRESHOLD").MustDuration(5 * time.Second)
} else {
Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(5 * time.Second)
}
} }
// DBConnStr returns database connection string // DBConnStr returns database connection string

View file

@ -64,7 +64,7 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET") LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
LFS.JWTSecretBytes, err = util.Base64FixedDecode(base64.RawURLEncoding, []byte(LFS.JWTSecretBase64), 32) LFS.JWTSecretBytes, err = util.Base64FixedDecode(base64.RawURLEncoding, []byte(LFS.JWTSecretBase64), 32)
if err != nil { if err != nil {
LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64() LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecret()
if err != nil { if err != nil {
return fmt.Errorf("error generating JWT Secret for custom config: %v", err) return fmt.Errorf("error generating JWT Secret for custom config: %v", err)
} }

View file

@ -138,12 +138,11 @@ func loadOAuth2From(rootCfg ConfigProvider) {
if InstallLock { if InstallLock {
if _, err := util.Base64FixedDecode(base64.RawURLEncoding, []byte(OAuth2.JWTSecretBase64), 32); err != nil { if _, err := util.Base64FixedDecode(base64.RawURLEncoding, []byte(OAuth2.JWTSecretBase64), 32); err != nil {
key, err := generate.NewJwtSecret() _, OAuth2.JWTSecretBase64, err = generate.NewJwtSecret()
if err != nil { if err != nil {
log.Fatal("error generating JWT secret: %v", err) log.Fatal("error generating JWT secret: %v", err)
} }
OAuth2.JWTSecretBase64 = base64.RawURLEncoding.EncodeToString(key)
saveCfg, err := rootCfg.PrepareSaving() saveCfg, err := rootCfg.PrepareSaving()
if err != nil { if err != nil {
log.Fatal("save oauth2.JWT_SECRET failed: %v", err) log.Fatal("save oauth2.JWT_SECRET failed: %v", err)

View file

@ -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"`
@ -109,6 +113,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",
@ -151,7 +158,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,
@ -161,6 +168,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,
@ -265,6 +273,8 @@ var (
Wiki: []string{"never"}, Wiki: []string{"never"},
DefaultTrustModel: "collaborator", DefaultTrustModel: "collaborator",
}, },
EnableFlags: false,
} }
RepoRootPath string RepoRootPath string
ScriptType = "bash" ScriptType = "bash"
@ -361,4 +371,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()
} }

View file

@ -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)

View file

@ -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)

View file

@ -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"`

View file

@ -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"`

View file

@ -88,6 +88,7 @@ type Repository struct {
ExternalTracker *ExternalTracker `json:"external_tracker,omitempty"` ExternalTracker *ExternalTracker `json:"external_tracker,omitempty"`
HasWiki bool `json:"has_wiki"` HasWiki bool `json:"has_wiki"`
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"` ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
WikiBranch string `json:"wiki_branch,omitempty"`
HasPullRequests bool `json:"has_pull_requests"` HasPullRequests bool `json:"has_pull_requests"`
HasProjects bool `json:"has_projects"` HasProjects bool `json:"has_projects"`
HasReleases bool `json:"has_releases"` HasReleases bool `json:"has_releases"`
@ -175,6 +176,8 @@ type EditRepoOption struct {
ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"` ExternalWiki *ExternalWiki `json:"external_wiki,omitempty"`
// sets the default branch for this repository. // sets the default branch for this repository.
DefaultBranch *string `json:"default_branch,omitempty"` DefaultBranch *string `json:"default_branch,omitempty"`
// sets the branch used for this repository's wiki.
WikiBranch *string `json:"wiki_branch,omitempty"`
// either `true` to allow pull requests, or `false` to prevent pull request. // either `true` to allow pull requests, or `false` to prevent pull request.
HasPullRequests *bool `json:"has_pull_requests,omitempty"` HasPullRequests *bool `json:"has_pull_requests,omitempty"`
// either `true` to enable project unit, or `false` to disable them. // either `true` to enable project unit, or `false` to disable them.

View 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"`
}

View file

@ -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
}, },

View file

@ -55,13 +55,15 @@ func (lc *LogChecker) checkLogEvent(event *log.EventFormatted) {
var checkerIndex int64 var checkerIndex int64
func NewLogChecker(namePrefix string) (logChecker *LogChecker, cancel func()) { func NewLogChecker(namePrefix string, level log.Level) (logChecker *LogChecker, cancel func()) {
logger := log.GetManager().GetLogger(namePrefix) logger := log.GetManager().GetLogger(namePrefix)
newCheckerIndex := atomic.AddInt64(&checkerIndex, 1) newCheckerIndex := atomic.AddInt64(&checkerIndex, 1)
writerName := namePrefix + "-" + fmt.Sprint(newCheckerIndex) writerName := namePrefix + "-" + fmt.Sprint(newCheckerIndex)
lc := &LogChecker{} lc := &LogChecker{}
lc.EventWriterBaseImpl = log.NewEventWriterBase(writerName, "test-log-checker", log.WriterMode{}) lc.EventWriterBaseImpl = log.NewEventWriterBase(writerName, "test-log-checker", log.WriterMode{
Level: level,
})
logger.AddWriters(lc) logger.AddWriters(lc)
return lc, func() { _ = logger.RemoveWriter(writerName) } return lc, func() { _ = logger.RemoveWriter(writerName) }
} }

View file

@ -12,8 +12,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestLogChecker(t *testing.T) { func TestLogCheckerInfo(t *testing.T) {
lc, cleanup := NewLogChecker(log.DEFAULT) lc, cleanup := NewLogChecker(log.DEFAULT, log.INFO)
defer cleanup() defer cleanup()
lc.Filter("First", "Third").StopMark("End") lc.Filter("First", "Third").StopMark("End")
@ -24,11 +24,13 @@ func TestLogChecker(t *testing.T) {
assert.False(t, stopped) assert.False(t, stopped)
log.Info("First") log.Info("First")
log.Debug("Third")
filtered, stopped = lc.Check(100 * time.Millisecond) filtered, stopped = lc.Check(100 * time.Millisecond)
assert.ElementsMatch(t, []bool{true, false}, filtered) assert.ElementsMatch(t, []bool{true, false}, filtered)
assert.False(t, stopped) assert.False(t, stopped)
log.Info("Second") log.Info("Second")
log.Debug("Third")
filtered, stopped = lc.Check(100 * time.Millisecond) filtered, stopped = lc.Check(100 * time.Millisecond)
assert.ElementsMatch(t, []bool{true, false}, filtered) assert.ElementsMatch(t, []bool{true, false}, filtered)
assert.False(t, stopped) assert.False(t, stopped)
@ -43,3 +45,14 @@ func TestLogChecker(t *testing.T) {
assert.ElementsMatch(t, []bool{true, true}, filtered) assert.ElementsMatch(t, []bool{true, true}, filtered)
assert.True(t, stopped) assert.True(t, stopped)
} }
func TestLogCheckerDebug(t *testing.T) {
lc, cleanup := NewLogChecker(log.DEFAULT, log.DEBUG)
defer cleanup()
lc.StopMark("End")
log.Debug("End")
_, stopped := lc.Check(100 * time.Millisecond)
assert.True(t, stopped)
}

View file

@ -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

Some files were not shown because too many files have changed in this diff Show more