Merge branch 'rebase-forgejo-dependency' into forgejo

This commit is contained in:
Earl Warren 2024-01-15 18:28:22 +00:00
commit e165ff8886
No known key found for this signature in database
GPG key ID: 0579CB2928A78A00
221 changed files with 7000 additions and 622 deletions

View file

@ -103,6 +103,8 @@ package "code.gitea.io/gitea/models/unittest"
func LoadFixtures func LoadFixtures
func Copy func Copy
func CopyDir func CopyDir
func NewMockWebServer
func NormalizedFullPath
func FixturesDir func FixturesDir
func fatalTestError func fatalTestError
func InitSettings func InitSettings

File diff suppressed because one or more lines are too long

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]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1467,6 +1484,8 @@ LEVEL = Info
;; ;;
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled ;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
;DEFAULT_EMAIL_NOTIFICATIONS = enabled ;DEFAULT_EMAIL_NOTIFICATIONS = enabled
;; Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false
;SEND_NOTIFICATION_EMAIL_ON_NEW_USER = false
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1780,9 +1799,6 @@ LEVEL = Info
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ;;
;AVATAR_UPLOAD_PATH = data/avatars
;REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars
;;
;; How Gitea deals with missing repository avatars ;; How Gitea deals with missing repository avatars
;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used ;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used
;REPOSITORY_AVATAR_FALLBACK = none ;REPOSITORY_AVATAR_FALLBACK = none

View file

@ -457,6 +457,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
- `MAX_IDLE_CONNS` **2**: Max idle database connections on connection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`. - `MAX_IDLE_CONNS` **2**: Max idle database connections on connection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`.
- `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071). - `CONN_MAX_LIFETIME` **0 or 3s**: Sets the maximum amount of time a DB connection may be reused - default is 0, meaning there is no limit (except on MySQL where it is 3s - see #6804 & #7071).
- `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically. - `AUTO_MIGRATION` **true**: Whether execute database models migrations automatically.
- `SLOW_QUERY_TRESHOLD` **5s**: Threshold value in seconds beyond which query execution time is logged as a warning in the xorm logger.
[^1]: It may be necessary to specify a hostport even when listening on a unix socket, as the port is part of the socket name. see [#24552](https://github.com/go-gitea/gitea/issues/24552#issuecomment-1681649367) for additional details. [^1]: It may be necessary to specify a hostport even when listening on a unix socket, as the port is part of the socket name. see [#24552](https://github.com/go-gitea/gitea/issues/24552#issuecomment-1681649367) for additional details.
@ -516,6 +517,7 @@ And the following unique queues:
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled - `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
- `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations. - `DISABLE_REGULAR_ORG_CREATION`: **false**: Disallow regular (non-admin) users from creating organizations.
- `SEND_NOTIFICATION_EMAIL_ON_NEW_USER`: **false**: Send an email to all admins when a new user signs up to inform the admins about this act.
## Security (`security`) ## Security (`security`)

8
go.mod
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-rc5 github.com/opencontainers/image-spec v1.1.0-rc5
github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
github.com/prometheus/client_golang v1.17.0 github.com/prometheus/client_golang v1.17.0
github.com/quasoft/websspi v1.1.2 github.com/quasoft/websspi v1.1.2
@ -100,7 +97,6 @@ require (
github.com/ulikunitz/xz v0.5.11 github.com/ulikunitz/xz v0.5.11
github.com/urfave/cli/v2 v2.26.0 github.com/urfave/cli/v2 v2.26.0
github.com/xanzy/go-gitlab v0.95.2 github.com/xanzy/go-gitlab v0.95.2
github.com/xeipuuv/gojsonschema v1.2.0
github.com/yohcop/openid-go v1.0.1 github.com/yohcop/openid-go v1.0.1
github.com/yuin/goldmark v1.6.0 github.com/yuin/goldmark v1.6.0
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
@ -232,6 +228,7 @@ require (
github.com/mholt/acmez v1.2.0 // indirect github.com/mholt/acmez v1.2.0 // indirect
github.com/miekg/dns v1.1.57 // indirect github.com/miekg/dns v1.1.57 // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
@ -247,6 +244,7 @@ require (
github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pierrec/lz4/v4 v4.1.19 // indirect github.com/pierrec/lz4/v4 v4.1.19 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/common v0.45.0 // indirect
@ -277,8 +275,6 @@ require (
github.com/valyala/fastjson v1.6.4 // indirect github.com/valyala/fastjson v1.6.4 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
github.com/zeebo/blake3 v0.2.3 // indirect github.com/zeebo/blake3 v0.2.3 // indirect

9
go.sum
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 v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
@ -839,13 +837,6 @@ github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23n
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=

View file

@ -309,6 +309,32 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
return commiter.Commit() return commiter.Commit()
} }
func GetLatestRun(ctx context.Context, repoID int64) (*ActionRun, error) {
var run ActionRun
has, err := db.GetEngine(ctx).Where("repo_id=?", repoID).OrderBy("id DESC").Limit(1).Get(&run)
if err != nil {
return nil, err
} else if !has {
return nil, fmt.Errorf("latest run: %w", util.ErrNotExist)
}
return &run, nil
}
func GetLatestRunForBranchAndWorkflow(ctx context.Context, repoID int64, branch, workflowFile, event string) (*ActionRun, error) {
var run ActionRun
q := db.GetEngine(ctx).Where("repo_id=?", repoID).And("ref=?", branch).And("workflow_id=?", workflowFile)
if event != "" {
q = q.And("event=?", event)
}
has, err := q.Desc("id").Get(&run)
if err != nil {
return nil, err
} else if !has {
return nil, util.NewNotExistErrorf("run with repo_id %d, ref %s, workflow_id %s", repoID, branch, workflowFile)
}
return &run, nil
}
func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) { func GetRunByID(ctx context.Context, id int64) (*ActionRun, error) {
var run ActionRun var run ActionRun
has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run) has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run)

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.SlowQueryTreshold > 0 {
xormEngine.AddHook(&SlowQueryHook{
Treshold: setting.Database.SlowQueryTreshold,
Logger: log.GetLogger("xorm"),
})
}
xormEngine.AddHook(&ErrorQueryHook{
Logger: log.GetLogger("xorm"),
})
SetDefaultEngine(ctx, xormEngine) SetDefaultEngine(ctx, xormEngine)
return nil return nil
} }
@ -299,3 +312,38 @@ func SetLogSQL(ctx context.Context, on bool) {
sess.Engine().ShowSQL(on) sess.Engine().ShowSQL(on)
} }
} }
type SlowQueryHook struct {
Treshold time.Duration
Logger log.Logger
}
var _ contexts.Hook = &SlowQueryHook{}
func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
return c.Ctx, nil
}
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
if c.ExecuteTime >= h.Treshold {
h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
}
return nil
}
type ErrorQueryHook struct {
Logger log.Logger
}
var _ contexts.Hook = &ErrorQueryHook{}
func (ErrorQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
return c.Ctx, nil
}
func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error {
if c.Err != nil {
h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err)
}
return nil
}

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")
lc.StopMark("[Slow SQL Query]")
defer cleanup()
e := db.GetEngine(db.DefaultContext)
engine, ok := e.(*xorm.Engine)
assert.True(t, ok)
// It's not possible to clean this up with XORM, but it's luckily not harmful
// to leave around.
engine.AddHook(&db.SlowQueryHook{
Treshold: time.Second * 10,
Logger: log.GetLogger("slow-query"),
})
// NOOP query.
e.Exec("SELECT 1 WHERE false;")
_, stopped := lc.Check(100 * time.Millisecond)
assert.False(t, stopped)
engine.AddHook(&db.SlowQueryHook{
Treshold: 0, // Every query should be logged.
Logger: log.GetLogger("slow-query"),
})
// NOOP query.
e.Exec("SELECT 1 WHERE false;")
_, stopped = lc.Check(100 * time.Millisecond)
assert.True(t, stopped)
}
func TestErrorQuery(t *testing.T) {
lc, cleanup := test.NewLogChecker("error-query")
lc.StopMark("[Error SQL Query]")
defer cleanup()
e := db.GetEngine(db.DefaultContext)
engine, ok := e.(*xorm.Engine)
assert.True(t, ok)
// It's not possible to clean this up with XORM, but it's luckily not harmful
// to leave around.
engine.AddHook(&db.ErrorQueryHook{
Logger: log.GetLogger("error-query"),
})
// Valid query.
e.Exec("SELECT 1 WHERE false;")
_, stopped := lc.Check(100 * time.Millisecond)
assert.False(t, stopped)
// Table doesn't exist.
e.Exec("SELECT column FROM table;")
_, stopped = lc.Check(100 * time.Millisecond)
assert.True(t, stopped)
}

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: 59
publisher_id: 2
tag_name: "v1.0"
lower_tag_name: "v1.0"
target: "main"
title: "v1.0"
sha1: "d8f53dfb33f6ccf4169c34970b5e747511c18beb"
num_commits: 1
is_draft: false
is_prerelease: false
is_tag: false
created_unix: 946684803

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

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
@ -1693,3 +1694,16 @@
size: 0 size: 0
is_fsck_enabled: true is_fsck_enabled: true
close_issues_via_commit_in_any_branch: false close_issues_via_commit_in_any_branch: false
-
id: 59
owner_id: 2
owner_name: user2
lower_name: repo59
name: repo59
default_branch: master
is_empty: false
is_archived: false
is_private: false
status: 0
num_issues: 0

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: 14 num_repos: 15
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,10 @@ var migrations = []*Migration{
NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable), NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable),
// v2 -> v3 // v2 -> v3
NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable), NewMigration("create the forgejo_auth_token table", forgejo_v1_20.CreateAuthorizationTokenTable),
// v3 -> v4
NewMigration("Add default_permissions to repo_unit", forgejo_v1_22.AddDefaultPermissionsToRepoUnit),
// v4 -> v5
NewMigration("create the forgejo_repo_flag table", forgejo_v1_22.CreateRepoFlagTable),
} }
// GetCurrentDBVersion returns the current Forgejo database version. // GetCurrentDBVersion returns the current Forgejo database version.

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

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

@ -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"
@ -129,3 +135,21 @@ func PushMirrorsIterate(ctx context.Context, limit int, f func(idx int, bean any
} }
return sess.Iterate(new(PushMirror), f) return sess.Iterate(new(PushMirror), f)
} }
// GetPushMirrorRemoteAddress returns the address of associated with a repository's given remote.
func GetPushMirrorRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git")
remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName)
if err != nil {
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)
}
u, err := giturl.Parse(remoteURL)
if err != nil {
return "", err
}
u.User = nil
return u.String(), nil
}

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

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.ReplaceAll(path, "/", "_"))
if liveMode {
liveURL := fmt.Sprintf("%s%s", liveServerBaseURL, path)
request, err := http.NewRequest(r.Method, liveURL, nil)
assert.NoError(t, err, "constructing an HTTP request to %s failed", liveURL)
for headerName, headerValues := range r.Header {
// do not pass on the encoding: let the Transport of the HTTP client handle that for us
if strings.ToLower(headerName) != "accept-encoding" {
for _, headerValue := range headerValues {
request.Header.Add(headerName, headerValue)
}
}
}
response, err := http.DefaultClient.Do(request)
assert.NoError(t, err, "HTTP request to %s failed: %s", liveURL)
fixture, err := os.Create(fixturePath)
assert.NoError(t, err, "failed to open the fixture file %s for writing", fixturePath)
defer fixture.Close()
fixtureWriter := bufio.NewWriter(fixture)
for headerName, headerValues := range response.Header {
for _, headerValue := range headerValues {
if !slices.Contains(ignoredHeaders, strings.ToLower(headerName)) {
_, err := fixtureWriter.WriteString(fmt.Sprintf("%s: %s\n", headerName, headerValue))
assert.NoError(t, err, "writing the header of the HTTP response to the fixture file failed")
}
}
}
_, err = fixtureWriter.WriteString("\n")
assert.NoError(t, err, "writing the header of the HTTP response to the fixture file failed")
fixtureWriter.Flush()
log.Info("Mock HTTP Server: writing response to %s", fixturePath)
_, err = io.Copy(fixture, response.Body)
assert.NoError(t, err, "writing the body of the HTTP response to %s failed", liveURL)
err = fixture.Sync()
assert.NoError(t, err, "writing the body of the HTTP response to the fixture file failed")
}
fixture, err := os.ReadFile(fixturePath)
assert.NoError(t, err, "missing mock HTTP response: "+fixturePath)
w.WriteHeader(http.StatusOK)
// replace any mention of the live HTTP service by the mocked host
stringFixture := strings.ReplaceAll(string(fixture), liveServerBaseURL, mockServerBaseURL)
// parse back the fixture file into a series of HTTP headers followed by response body
lines := strings.Split(stringFixture, "\n")
for idx, line := range lines {
colonIndex := strings.Index(line, ": ")
if colonIndex != -1 {
w.Header().Set(line[0:colonIndex], line[colonIndex+2:])
} else {
// we reached the end of the headers (empty line), so what follows is the body
responseBody := strings.Join(lines[idx+1:], "\n")
_, err := w.Write([]byte(responseBody))
assert.NoError(t, err, "writing the body of the HTTP response failed")
break
}
}
}))
mockServerBaseURL = server.URL
return server
}
func NormalizedFullPath(url *url.URL) string {
// TODO normalize path (remove trailing slash?)
// TODO normalize RawQuery (order query parameters?)
if len(url.Query()) == 0 {
return url.EscapedPath()
}
return fmt.Sprintf("%s?%s", url.EscapedPath(), url.RawQuery)
}

View file

@ -189,6 +189,25 @@ func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error)
return emails, nil return emails, nil
} }
type ActivatedEmailAddress struct {
ID int64
Email string
}
func GetActivatedEmailAddresses(ctx context.Context, uid int64) ([]*ActivatedEmailAddress, error) {
emails := make([]*ActivatedEmailAddress, 0, 8)
if err := db.GetEngine(ctx).
Table("email_address").
Select("id, email").
Where("uid=?", uid).
And("is_activated=?", true).
Asc("id").
Find(&emails); err != nil {
return nil, err
}
return emails, nil
}
// GetEmailAddressByID gets a user's email address by ID // GetEmailAddressByID gets a user's email address by ID
func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, error) { func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, error) {
// User ID is required for security reasons // User ID is required for security reasons
@ -356,31 +375,7 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e
return UpdateUserCols(ctx, user, "rands") return UpdateUserCols(ctx, user, "rands")
} }
// MakeEmailPrimary sets primary email address of given user. func makeEmailPrimary(ctx context.Context, user *User, email *EmailAddress) error {
func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
has, err := db.GetEngine(ctx).Get(email)
if err != nil {
return err
} else if !has {
return ErrEmailAddressNotExist{Email: email.Email}
}
if !email.IsActivated {
return ErrEmailNotActivated
}
user := &User{}
has, err = db.GetEngine(ctx).ID(email.UID).Get(user)
if err != nil {
return err
} else if !has {
return ErrUserNotExist{
UID: email.UID,
Name: "",
KeyID: 0,
}
}
ctx, committer, err := db.TxContext(ctx) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return err return err
@ -410,6 +405,57 @@ func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
return committer.Commit() return committer.Commit()
} }
// ReplaceInactivePrimaryEmail replaces the primary email of a given user, even if the primary is not yet activated.
func ReplaceInactivePrimaryEmail(ctx context.Context, oldEmail string, email *EmailAddress) error {
user := &User{}
has, err := db.GetEngine(ctx).ID(email.UID).Get(user)
if err != nil {
return err
} else if !has {
return ErrUserNotExist{
UID: email.UID,
Name: "",
KeyID: 0,
}
}
err = AddEmailAddress(ctx, email)
if err != nil {
return err
}
err = makeEmailPrimary(ctx, user, email)
if err != nil {
return err
}
return DeleteEmailAddress(ctx, &EmailAddress{UID: email.UID, Email: oldEmail})
}
// MakeEmailPrimary sets primary email address of given user.
func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
has, err := db.GetEngine(ctx).Get(email)
if err != nil {
return err
} else if !has {
return ErrEmailAddressNotExist{Email: email.Email}
}
if !email.IsActivated {
return ErrEmailNotActivated
}
user := &User{}
has, err = db.GetEngine(ctx).ID(email.UID).Get(user)
if err != nil {
return err
} else if !has {
return ErrUserNotExist{UID: email.UID}
}
return makeEmailPrimary(ctx, user, email)
}
// VerifyActiveEmailCode verifies active email code when active account // VerifyActiveEmailCode verifies active email code when active account
func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress { func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
minutes := setting.Service.ActiveCodeLives minutes := setting.Service.ActiveCodeLives

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"
@ -166,6 +167,28 @@ func TestMakeEmailPrimary(t *testing.T) {
assert.Equal(t, "user101@example.com", user.Email) assert.Equal(t, "user101@example.com", user.Email)
} }
func TestReplaceInactivePrimaryEmail(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
email := &user_model.EmailAddress{
Email: "user9999999@example.com",
UID: 9999999,
}
err := user_model.ReplaceInactivePrimaryEmail(db.DefaultContext, "user10@example.com", email)
assert.Error(t, err)
assert.True(t, user_model.IsErrUserNotExist(err))
email = &user_model.EmailAddress{
Email: "user201@example.com",
UID: 10,
}
err = user_model.ReplaceInactivePrimaryEmail(db.DefaultContext, "user10@example.com", email)
assert.NoError(t, err)
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 10})
assert.Equal(t, "user201@example.com", user.Email)
}
func TestActivate(t *testing.T) { func TestActivate(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())
@ -309,3 +332,37 @@ func TestEmailAddressValidate(t *testing.T) {
}) })
} }
} }
func TestGetActivatedEmailAddresses(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
testCases := []struct {
UID int64
expected []*user_model.ActivatedEmailAddress
}{
{
UID: 1,
expected: []*user_model.ActivatedEmailAddress{{ID: 9, Email: "user1@example.com"}, {ID: 33, Email: "user1-2@example.com"}, {ID: 34, Email: "user1-3@example.com"}},
},
{
UID: 2,
expected: []*user_model.ActivatedEmailAddress{{ID: 3, Email: "user2@example.com"}},
},
{
UID: 4,
expected: []*user_model.ActivatedEmailAddress{{ID: 11, Email: "user4@example.com"}},
},
{
UID: 11,
expected: []*user_model.ActivatedEmailAddress{},
},
}
for _, testCase := range testCases {
t.Run(fmt.Sprintf("User %d", testCase.UID), func(t *testing.T) {
emails, err := user_model.GetActivatedEmailAddresses(db.DefaultContext, testCase.UID)
assert.NoError(t, err)
assert.Equal(t, testCase.expected, emails)
})
}
}

View file

@ -228,6 +228,12 @@ func GetAllUsers(ctx context.Context) ([]*User, error) {
return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).Find(&users) return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).Find(&users)
} }
// GetAllAdmins returns a slice of all adminusers found in DB.
func GetAllAdmins(ctx context.Context) ([]*User, error) {
users := make([]*User, 0)
return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).And("is_admin = ?", true).Find(&users)
}
// IsLocal returns true if user login type is LoginPlain. // IsLocal returns true if user login type is LoginPlain.
func (u *User) IsLocal() bool { func (u *User) IsLocal() bool {
return u.LoginType <= auth.Plain return u.LoginType <= auth.Plain

View file

@ -533,6 +533,16 @@ func TestIsUserVisibleToViewer(t *testing.T) {
test(user31, nil, false) test(user31, nil, false)
} }
func TestGetAllAdmins(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
admins, err := user_model.GetAllAdmins(db.DefaultContext)
assert.NoError(t, err)
assert.Len(t, admins, 1)
assert.Equal(t, int64(1), admins[0].ID)
}
func Test_ValidateUser(t *testing.T) { func Test_ValidateUser(t *testing.T) {
oldSetting := setting.Service.AllowedUserVisibilityModesSlice oldSetting := setting.Service.AllowedUserVisibilityModesSlice
defer func() { defer func() {
@ -552,6 +562,11 @@ func Test_ValidateUser(t *testing.T) {
} }
func Test_NormalizeUserFromEmail(t *testing.T) { func Test_NormalizeUserFromEmail(t *testing.T) {
oldSetting := setting.Service.AllowDotsInUsernames
defer func() {
setting.Service.AllowDotsInUsernames = oldSetting
}()
setting.Service.AllowDotsInUsernames = true
testCases := []struct { testCases := []struct {
Input string Input string
Expected string Expected string

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"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
@ -38,6 +39,7 @@ type APIContext struct {
ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer ContextUser *user_model.User // the user which is being visited, in most cases it differs from Doer
Repo *Repository Repo *Repository
Comment *issues_model.Comment
Org *APIOrganization Org *APIOrganization
Package *Package Package *Package
} }

View file

@ -0,0 +1,91 @@
// Copyright 2023 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
// SPDX-License-Identifier: MIT
package doctor
import (
"context"
"strings"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/log"
"xorm.io/builder"
)
func FixPushMirrorsWithoutGitRemote(ctx context.Context, logger log.Logger, autofix bool) error {
var missingMirrors []*repo_model.PushMirror
err := db.Iterate(ctx, builder.Gt{"id": 0}, func(ctx context.Context, repo *repo_model.Repository) error {
pushMirrors, _, err := repo_model.GetPushMirrorsByRepoID(ctx, repo.ID, db.ListOptions{})
if err != nil {
return err
}
for i := 0; i < len(pushMirrors); i++ {
_, err = repo_model.GetPushMirrorRemoteAddress(repo.OwnerName, repo.Name, pushMirrors[i].RemoteName)
if err != nil {
if strings.Contains(err.Error(), "No such remote") {
missingMirrors = append(missingMirrors, pushMirrors[i])
} else if logger != nil {
logger.Warn("Unable to retrieve the remote address of a mirror: %s", err)
}
}
}
return nil
})
if err != nil {
if logger != nil {
logger.Critical("Unable to iterate across repounits to fix push mirrors without a git remote: Error %v", err)
}
return err
}
count := len(missingMirrors)
if !autofix {
if logger != nil {
if count == 0 {
logger.Info("Found no push mirrors with missing git remotes")
} else {
logger.Warn("Found %d push mirrors with missing git remotes", count)
}
}
return nil
}
for i := 0; i < len(missingMirrors); i++ {
if logger != nil {
logger.Info("Removing push mirror #%d (remote: %s), for repo: %s/%s",
missingMirrors[i].ID,
missingMirrors[i].RemoteName,
missingMirrors[i].GetRepository(ctx).OwnerName,
missingMirrors[i].GetRepository(ctx).Name)
}
err = repo_model.DeletePushMirrors(ctx, repo_model.PushMirrorOptions{
ID: missingMirrors[i].ID,
RepoID: missingMirrors[i].RepoID,
RemoteName: missingMirrors[i].RemoteName,
})
if err != nil {
if logger != nil {
logger.Critical("Error removing a push mirror (repo_id: %d, push_mirror: %d): %s", missingMirrors[i].Repo.ID, missingMirrors[i].ID, err)
}
return err
}
}
return nil
}
func init() {
Register(&Check{
Title: "Check for push mirrors without a git remote configured",
Name: "fix-push-mirrors-without-git-remote",
IsDefault: false,
Run: FixPushMirrorsWithoutGitRemote,
Priority: 7,
})
}

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

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

@ -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
SlowQueryTreshold time.Duration
}{ }{
Timeout: 500, Timeout: 500,
IterateBufferSize: 50, IterateBufferSize: 50,
@ -87,6 +88,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10) Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second) Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true) Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
Database.SlowQueryTreshold = sec.Key("SLOW_QUERY_TRESHOLD").MustDuration(5 * time.Second)
} }
// DBConnStr returns database connection string // DBConnStr returns database connection string

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"`
@ -108,6 +112,9 @@ var (
Wiki []string Wiki []string
DefaultTrustModel string DefaultTrustModel string
} `ini:"repository.signing"` } `ini:"repository.signing"`
SettableFlags []string
EnableFlags bool
}{ }{
DetectedCharsetsOrder: []string{ DetectedCharsetsOrder: []string{
"UTF-8", "UTF-8",
@ -150,7 +157,7 @@ var (
DefaultPrivate: RepoCreatingLastUserVisibility, DefaultPrivate: RepoCreatingLastUserVisibility,
DefaultPushCreatePrivate: true, DefaultPushCreatePrivate: true,
MaxCreationLimit: -1, MaxCreationLimit: -1,
PreferredLicenses: []string{"Apache License 2.0", "MIT License"}, PreferredLicenses: []string{"Apache-2.0", "MIT"},
DisableHTTPGit: false, DisableHTTPGit: false,
AccessControlAllowOrigin: "", AccessControlAllowOrigin: "",
UseCompatSSHURI: false, UseCompatSSHURI: false,
@ -160,6 +167,7 @@ var (
DisabledRepoUnits: []string{}, DisabledRepoUnits: []string{},
DefaultRepoUnits: []string{}, DefaultRepoUnits: []string{},
DefaultForkRepoUnits: []string{}, DefaultForkRepoUnits: []string{},
DownloadOrCloneMethods: []string{"download-zip", "download-targz", "download-bundle", "vscode-clone"},
PrefixArchiveFiles: true, PrefixArchiveFiles: true,
DisableMigrations: false, DisableMigrations: false,
DisableStars: false, DisableStars: false,
@ -262,6 +270,8 @@ var (
Wiki: []string{"never"}, Wiki: []string{"never"},
DefaultTrustModel: "collaborator", DefaultTrustModel: "collaborator",
}, },
EnableFlags: false,
} }
RepoRootPath string RepoRootPath string
ScriptType = "bash" ScriptType = "bash"
@ -358,4 +368,12 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
if err := loadRepoArchiveFrom(rootCfg); err != nil { if err := loadRepoArchiveFrom(rootCfg); err != nil {
log.Fatal("loadRepoArchiveFrom: %v", err) log.Fatal("loadRepoArchiveFrom: %v", err)
} }
for _, method := range Repository.DownloadOrCloneMethods {
if !slices.Contains(RecognisedRepositoryDownloadOrCloneMethods, method) {
log.Error("Unrecognised repository download or clone method: %s", method)
}
}
Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool()
} }

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

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

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

View file

@ -7,12 +7,12 @@ import (
"crypto" "crypto"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"regexp" "regexp"
"testing" "testing"
"github.com/minio/sha256-simd"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

View file

@ -117,13 +117,20 @@ func IsValidExternalTrackerURLFormat(uri string) bool {
} }
var ( var (
validUsernamePattern = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`) validUsernamePatternWithDots = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`)
invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) // No consecutive or trailing non-alphanumeric chars validUsernamePatternWithoutDots = regexp.MustCompile(`^[\da-zA-Z][-\w]*$`)
// No consecutive or trailing non-alphanumeric chars, catches both cases
invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`)
) )
// IsValidUsername checks if username is valid // IsValidUsername checks if username is valid
func IsValidUsername(name string) bool { func IsValidUsername(name string) bool {
// It is difficult to find a single pattern that is both readable and effective, // It is difficult to find a single pattern that is both readable and effective,
// but it's easier to use positive and negative checks. // but it's easier to use positive and negative checks.
return validUsernamePattern.MatchString(name) && !invalidUsernamePattern.MatchString(name) if setting.Service.AllowDotsInUsernames {
return validUsernamePatternWithDots.MatchString(name) && !invalidUsernamePattern.MatchString(name)
}
return validUsernamePatternWithoutDots.MatchString(name) && !invalidUsernamePattern.MatchString(name)
} }

View file

@ -155,7 +155,8 @@ func Test_IsValidExternalTrackerURLFormat(t *testing.T) {
} }
} }
func TestIsValidUsername(t *testing.T) { func TestIsValidUsernameAllowDots(t *testing.T) {
setting.Service.AllowDotsInUsernames = true
tests := []struct { tests := []struct {
arg string arg string
want bool want bool
@ -185,3 +186,31 @@ func TestIsValidUsername(t *testing.T) {
}) })
} }
} }
func TestIsValidUsernameBanDots(t *testing.T) {
setting.Service.AllowDotsInUsernames = false
defer func() {
setting.Service.AllowDotsInUsernames = true
}()
tests := []struct {
arg string
want bool
}{
{arg: "a", want: true},
{arg: "abc", want: true},
{arg: "0.b-c", want: false},
{arg: "a.b-c_d", want: false},
{arg: ".abc", want: false},
{arg: "abc.", want: false},
{arg: "a..bc", want: false},
{arg: "a...bc", want: false},
{arg: "a.-bc", want: false},
{arg: "a._bc", want: false},
}
for _, tt := range tests {
t.Run(tt.arg, func(t *testing.T) {
assert.Equalf(t, tt.want, IsValidUsername(tt.arg), "IsValidUsername[AllowDotsInUsernames=false](%v)", tt.arg)
})
}
}

View file

@ -147,6 +147,16 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
} }
} }
if hp, ok := handler.(func(next http.Handler) http.HandlerFunc); ok {
return func(next http.Handler) http.Handler {
h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
routing.UpdateFuncInfo(req.Context(), funcInfo)
h.ServeHTTP(resp, req)
})
}
}
provider := func(next http.Handler) http.Handler { provider := func(next http.Handler) http.Handler {
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
// wrap the response writer to check whether the response has been written // wrap the response writer to check whether the response has been written

View file

@ -8,6 +8,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation" "code.gitea.io/gitea/modules/validation"
@ -135,7 +136,11 @@ func Validate(errs binding.Errors, data map[string]any, f Form, l translation.Lo
case validation.ErrRegexPattern: case validation.ErrRegexPattern:
data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message) data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
case validation.ErrUsername: case validation.ErrUsername:
if setting.Service.AllowDotsInUsernames {
data["ErrorMsg"] = trName + l.Tr("form.username_error") data["ErrorMsg"] = trName + l.Tr("form.username_error")
} else {
data["ErrorMsg"] = trName + l.Tr("form.username_error_no_dots")
}
case validation.ErrInvalidGroupTeamMap: case validation.ErrInvalidGroupTeamMap:
data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message) data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
default: default:

View file

@ -53,6 +53,7 @@ func CommonTemplateContextData() ContextData {
"ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage, "ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage,
"ShowFooterVersion": setting.Other.ShowFooterVersion, "ShowFooterVersion": setting.Other.ShowFooterVersion,
"DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives, "DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives,
"DownloadOrCloneMethods": setting.Repository.DownloadOrCloneMethods,
"EnableSwagger": setting.API.EnableSwagger, "EnableSwagger": setting.API.EnableSwagger,
"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn, "EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn,

View file

@ -295,6 +295,7 @@ default_allow_create_organization = Allow Creation of Organizations by Default
default_allow_create_organization_popup = Allow new user accounts to create organizations by default. default_allow_create_organization_popup = Allow new user accounts to create organizations by default.
default_enable_timetracking = Enable Time Tracking by Default default_enable_timetracking = Enable Time Tracking by Default
default_enable_timetracking_popup = Enable time tracking for new repositories by default. default_enable_timetracking_popup = Enable time tracking for new repositories by default.
allow_dots_in_usernames = Allow users to use dots in their usernames. Doesn't affect existing accounts.
no_reply_address = Hidden Email Domain no_reply_address = Hidden Email Domain
no_reply_address_helper = Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'. no_reply_address_helper = Domain name for users with a hidden email address. For example, the username 'joe' will be logged in Git as 'joe@noreply.example.org' if the hidden email domain is set to 'noreply.example.org'.
password_algorithm = Password Hash Algorithm password_algorithm = Password Hash Algorithm
@ -367,7 +368,7 @@ forgot_password_title= Forgot Password
forgot_password = Forgot password? forgot_password = Forgot password?
sign_up_now = Need an account? Register now. sign_up_now = Need an account? Register now.
sign_up_successful = Account was successfully created. Welcome! sign_up_successful = Account was successfully created. Welcome!
confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. If the email is incorrect, you can log in, and request another confirmation email to be sent to a different address.
must_change_password = Update your password must_change_password = Update your password
allow_password_change = Require user to change password (recommended) allow_password_change = Require user to change password (recommended)
reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the account recovery process. reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the account recovery process.
@ -377,6 +378,9 @@ prohibit_login = Sign In Prohibited
prohibit_login_desc = Your account is prohibited from signing in, please contact your site administrator. prohibit_login_desc = Your account is prohibited from signing in, please contact your site administrator.
resent_limit_prompt = You have already requested an activation email recently. Please wait 3 minutes and try again. resent_limit_prompt = You have already requested an activation email recently. Please wait 3 minutes and try again.
has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to resend a new one, please click on the button below. has_unconfirmed_mail = Hi %s, you have an unconfirmed email address (<b>%s</b>). If you haven't received a confirmation email or need to resend a new one, please click on the button below.
change_unconfirmed_email_summary = Change the email address activation mail is sent to.
change_unconfirmed_email = If you have given the wrong email address during registration, you can change it below, and a confirmation will be sent to the new address instead.
change_unconfirmed_email_error = Unable to change the email address: %v
resend_mail = Click here to resend your activation email resend_mail = Click here to resend your activation email
email_not_associate = The email address is not associated with any account. email_not_associate = The email address is not associated with any account.
send_reset_mail = Send Account Recovery Email send_reset_mail = Send Account Recovery Email
@ -441,6 +445,10 @@ activate_email = Verify your email address
activate_email.title = %s, please verify your email address activate_email.title = %s, please verify your email address
activate_email.text = Please click the following link to verify your email address within <b>%s</b>: activate_email.text = Please click the following link to verify your email address within <b>%s</b>:
admin.new_user.subject = New user %s just signed up
admin.new_user.user_info = User Information
admin.new_user.text = Please <a href="%s">click here</a> to manage the user from the admin panel.
register_notify = Welcome to Gitea register_notify = Welcome to Gitea
register_notify.title = %[1]s, welcome to %[2]s register_notify.title = %[1]s, welcome to %[2]s
register_notify.text_1 = this is your registration confirmation email for %s! register_notify.text_1 = this is your registration confirmation email for %s!
@ -535,6 +543,7 @@ include_error = ` must contain substring "%s".`
glob_pattern_error = ` glob pattern is invalid: %s.` glob_pattern_error = ` glob pattern is invalid: %s.`
regex_pattern_error = ` regex pattern is invalid: %s.` regex_pattern_error = ` regex pattern is invalid: %s.`
username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.` username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.`
username_error_no_dots = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-') and underscore ('_'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.`
invalid_group_team_map_error = ` mapping is invalid: %s` invalid_group_team_map_error = ` mapping is invalid: %s`
unknown_error = Unknown error: unknown_error = Unknown error:
captcha_incorrect = The CAPTCHA code is incorrect. captcha_incorrect = The CAPTCHA code is incorrect.
@ -944,6 +953,14 @@ user_unblock_success = The user has been unblocked successfully.
user_block_success = The user has been blocked successfully. user_block_success = The user has been blocked successfully.
[repo] [repo]
rss.must_be_on_branch = You must be on a branch to have an RSS feed.
admin.manage_flags = Manage flags
admin.enabled_flags = Flags enabled for the repository:
admin.update_flags = Update flags
admin.failed_to_replace_flags = Failed to replace repository flags
admin.flags_replaced = Repository flags replaced
new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? <a href="%s">Migrate repository.</a> new_repo_helper = A repository contains all project files, including revision history. Already hosting one elsewhere? <a href="%s">Migrate repository.</a>
owner = Owner owner = Owner
owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit. owner_helper = Some organizations may not show up in the dropdown due to a maximum repository count limit.
@ -970,6 +987,7 @@ all_branches = All branches
fork_no_valid_owners = This repository can not be forked because there are no valid owners. fork_no_valid_owners = This repository can not be forked because there are no valid owners.
use_template = Use this template use_template = Use this template
clone_in_vsc = Clone in VS Code clone_in_vsc = Clone in VS Code
clone_in_vscodium = Clone in VS Codium
download_zip = Download ZIP download_zip = Download ZIP
download_tar = Download TAR.GZ download_tar = Download TAR.GZ
download_bundle = Download BUNDLE download_bundle = Download BUNDLE
@ -1256,6 +1274,7 @@ editor.new_branch_name_desc = New branch name…
editor.cancel = Cancel editor.cancel = Cancel
editor.filename_cannot_be_empty = The filename cannot be empty. editor.filename_cannot_be_empty = The filename cannot be empty.
editor.filename_is_invalid = The filename is invalid: "%s". editor.filename_is_invalid = The filename is invalid: "%s".
editor.invalid_commit_mail = Invalid mail for creating a commit.
editor.branch_does_not_exist = Branch "%s" does not exist in this repository. editor.branch_does_not_exist = Branch "%s" does not exist in this repository.
editor.branch_already_exists = Branch "%s" already exists in this repository. editor.branch_already_exists = Branch "%s" already exists in this repository.
editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository. editor.directory_is_a_file = Directory name "%s" is already used as a filename in this repository.
@ -1294,6 +1313,8 @@ commits.find = Search
commits.search_all = All Branches commits.search_all = All Branches
commits.author = Author commits.author = Author
commits.message = Message commits.message = Message
commits.browse_further = Browse further
commits.renamed_from = Renamed from %s
commits.date = Date commits.date = Date
commits.older = Older commits.older = Older
commits.newer = Newer commits.newer = Newer
@ -1843,7 +1864,7 @@ pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull re
pulls.delete.title = Delete this pull request? pulls.delete.title = Delete this pull request?
pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived) pulls.delete.text = Do you really want to delete this pull request? (This will permanently remove all content. Consider closing it instead, if you intend to keep it archived)
pulls.recently_pushed_new_branches = You pushed on branch <strong>%[1]s</strong> %[2]s pulls.recently_pushed_new_branches = You pushed on branch <a href="%[3]s"><strong>%[1]s</strong></a> %[2]s
pull.deleted_branch = (deleted):%s pull.deleted_branch = (deleted):%s
@ -1906,6 +1927,7 @@ wiki.page_title = Page title
wiki.page_content = Page content wiki.page_content = Page content
wiki.default_commit_message = Write a note about this page update (optional). wiki.default_commit_message = Write a note about this page update (optional).
wiki.save_page = Save Page wiki.save_page = Save Page
wiki.cancel = Cancel
wiki.last_commit_info = %s edited this page %s wiki.last_commit_info = %s edited this page %s
wiki.edit_page_button = Edit wiki.edit_page_button = Edit
wiki.new_page_button = New Page wiki.new_page_button = New Page
@ -2044,6 +2066,7 @@ settings.branches.update_default_branch = Update Default Branch
settings.branches.add_new_rule = Add New Rule settings.branches.add_new_rule = Add New Rule
settings.advanced_settings = Advanced Settings settings.advanced_settings = Advanced Settings
settings.wiki_desc = Enable Repository Wiki settings.wiki_desc = Enable Repository Wiki
settings.wiki_globally_editable = Allow anyone to edit the Wiki
settings.use_internal_wiki = Use Built-In Wiki settings.use_internal_wiki = Use Built-In Wiki
settings.use_external_wiki = Use External Wiki settings.use_external_wiki = Use External Wiki
settings.external_wiki_url = External Wiki URL settings.external_wiki_url = External Wiki URL

View file

@ -8,6 +8,7 @@ import (
"crypto" "crypto"
"crypto/rsa" "crypto/rsa"
"crypto/sha1" "crypto/sha1"
"crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/pem" "encoding/pem"
@ -26,8 +27,6 @@ import (
chef_module "code.gitea.io/gitea/modules/packages/chef" chef_module "code.gitea.io/gitea/modules/packages/chef"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth"
"github.com/minio/sha256-simd"
) )
const ( const (

View file

@ -6,6 +6,7 @@ package maven
import ( import (
"crypto/md5" "crypto/md5"
"crypto/sha1" "crypto/sha1"
"crypto/sha256"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
"encoding/xml" "encoding/xml"
@ -26,8 +27,6 @@ import (
maven_module "code.gitea.io/gitea/modules/packages/maven" maven_module "code.gitea.io/gitea/modules/packages/maven"
"code.gitea.io/gitea/routers/api/packages/helper" "code.gitea.io/gitea/routers/api/packages/helper"
packages_service "code.gitea.io/gitea/services/packages" packages_service "code.gitea.io/gitea/services/packages"
"github.com/minio/sha256-simd"
) )
const ( const (

View file

@ -6,7 +6,7 @@
// //
// This documentation describes the Gitea API. // This documentation describes the Gitea API.
// //
// Schemes: http, https // Schemes: https, http
// BasePath: /api/v1 // BasePath: /api/v1
// Version: {{AppVer | JSEscape | Safe}} // Version: {{AppVer | JSEscape | Safe}}
// License: MIT http://opensource.org/licenses/MIT // License: MIT http://opensource.org/licenses/MIT
@ -73,6 +73,7 @@ import (
actions_model "code.gitea.io/gitea/models/actions" actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth" auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm" "code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access" access_model "code.gitea.io/gitea/models/perm/access"
@ -230,6 +231,39 @@ func repoAssignment() func(ctx *context.APIContext) {
} }
} }
// must be used within a group with a call to repoAssignment() to set ctx.Repo
func commentAssignment(idParam string) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(idParam))
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.NotFound(err)
} else {
ctx.InternalServerError(err)
}
return
}
if err = comment.LoadIssue(ctx); err != nil {
ctx.InternalServerError(err)
return
}
if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound()
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.NotFound()
return
}
comment.Issue.Repo = ctx.Repo.Repository
ctx.Comment = comment
}
}
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) { func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) { return func(ctx *context.APIContext) {
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
@ -1104,6 +1138,18 @@ func Routes() *web.Route {
m.Get("/permission", repo.GetRepoPermissions) m.Get("/permission", repo.GetRepoPermissions)
}) })
}, reqToken()) }, reqToken())
if setting.Repository.EnableFlags {
m.Group("/flags", func() {
m.Combo("").Get(repo.ListFlags).
Put(bind(api.ReplaceFlagsOption{}), repo.ReplaceAllFlags).
Delete(repo.DeleteAllFlags)
m.Group("/{flag}", func() {
m.Combo("").Get(repo.HasFlag).
Put(repo.AddFlag).
Delete(repo.DeleteFlag)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
}
m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees) m.Get("/assignees", reqToken(), reqAnyRepoReader(), repo.GetAssignees)
m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers) m.Get("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
m.Group("/teams", func() { m.Group("/teams", func() {
@ -1223,8 +1269,12 @@ func Routes() *web.Route {
Get(repo.GetPullReview). Get(repo.GetPullReview).
Delete(reqToken(), repo.DeletePullReview). Delete(reqToken(), repo.DeletePullReview).
Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview) Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
m.Combo("/comments"). m.Group("/comments", func() {
Get(repo.GetPullReviewComments) m.Combo("").
Get(repo.GetPullReviewComments).
Post(reqToken(), bind(api.CreatePullReviewCommentOptions{}), repo.CreatePullReviewComment)
m.Get("/{comment}", commentAssignment("comment"), repo.GetPullReviewComment)
})
m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview) m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
m.Post("/undismissals", reqToken(), repo.UnDismissPullReview) m.Post("/undismissals", reqToken(), repo.UnDismissPullReview)
}) })
@ -1328,7 +1378,7 @@ func Routes() *web.Route {
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment). Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueCommentAttachment) Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueCommentAttachment)
}, mustEnableAttachments) }, mustEnableAttachments)
}) }, commentAssignment(":id"))
}) })
m.Group("/{index}", func() { m.Group("/{index}", func() {
m.Combo("").Get(repo.GetIssue). m.Combo("").Get(repo.GetIssue).

View file

@ -0,0 +1,245 @@
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
)
func ListFlags(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/flags repository repoListFlags
// ---
// summary: List a repository's flags
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/StringSlice"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
repoFlags, err := ctx.Repo.Repository.ListFlags(ctx)
if err != nil {
ctx.InternalServerError(err)
return
}
flags := make([]string, len(repoFlags))
for i := range repoFlags {
flags[i] = repoFlags[i].Name
}
ctx.SetTotalCountHeader(int64(len(repoFlags)))
ctx.JSON(http.StatusOK, flags)
}
func ReplaceAllFlags(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/flags repository repoReplaceAllFlags
// ---
// summary: Replace all flags of a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/ReplaceFlagsOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
flagsForm := web.GetForm(ctx).(*api.ReplaceFlagsOption)
if err := ctx.Repo.Repository.ReplaceAllFlags(ctx, flagsForm.Flags); err != nil {
ctx.InternalServerError(err)
return
}
ctx.Status(http.StatusNoContent)
}
func DeleteAllFlags(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/flags repository repoDeleteAllFlags
// ---
// summary: Remove all flags from a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
if err := ctx.Repo.Repository.ReplaceAllFlags(ctx, nil); err != nil {
ctx.InternalServerError(err)
return
}
ctx.Status(http.StatusNoContent)
}
func HasFlag(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/flags/{flag} repository repoCheckFlag
// ---
// summary: Check if a repository has a given flag
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: flag
// in: path
// description: name of the flag
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
hasFlag := ctx.Repo.Repository.HasFlag(ctx, ctx.Params(":flag"))
if hasFlag {
ctx.Status(http.StatusNoContent)
} else {
ctx.NotFound()
}
}
func AddFlag(ctx *context.APIContext) {
// swagger:operation PUT /repos/{owner}/{repo}/flags/{flag} repository repoAddFlag
// ---
// summary: Add a flag to a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: flag
// in: path
// description: name of the flag
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
flag := ctx.Params(":flag")
if ctx.Repo.Repository.HasFlag(ctx, flag) {
ctx.Status(http.StatusNoContent)
return
}
if err := ctx.Repo.Repository.AddFlag(ctx, flag); err != nil {
ctx.InternalServerError(err)
return
}
ctx.Status(http.StatusNoContent)
}
func DeleteFlag(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/flags/{flag} repository repoDeleteFlag
// ---
// summary: Remove a flag from a repository
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: flag
// in: path
// description: name of the flag
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
flag := ctx.Params(":flag")
if _, err := ctx.Repo.Repository.DeleteFlag(ctx, flag); err != nil {
ctx.InternalServerError(err)
return
}
ctx.Status(http.StatusNoContent)
}

View file

@ -454,29 +454,7 @@ func GetIssueComment(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) comment := ctx.Comment
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
}
return
}
if err = comment.LoadIssue(ctx); err != nil {
ctx.InternalServerError(err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.NotFound()
return
}
if comment.Type != issues_model.CommentTypeComment { if comment.Type != issues_model.CommentTypeComment {
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
@ -587,25 +565,7 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) {
} }
func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) { func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) {
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) comment := ctx.Comment
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
}
return
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden) ctx.Status(http.StatusForbidden)
@ -617,7 +577,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
return return
} }
err = comment.LoadIssue(ctx) err := comment.LoadIssue(ctx)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err) ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return return
@ -711,25 +671,7 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) {
} }
func deleteIssueComment(ctx *context.APIContext) { func deleteIssueComment(ctx *context.APIContext) {
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) comment := ctx.Comment
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
}
return
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Status(http.StatusNotFound)
return
}
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) { if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
ctx.Status(http.StatusForbidden) ctx.Status(http.StatusForbidden)
@ -739,7 +681,7 @@ func deleteIssueComment(ctx *context.APIContext) {
return return
} }
if err = issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil { if err := issue_service.DeleteComment(ctx, ctx.Doer, comment); err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err) ctx.Error(http.StatusInternalServerError, "DeleteCommentByID", err)
return return
} }

View file

@ -55,11 +55,8 @@ func GetIssueCommentAttachment(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
comment := getIssueCommentSafe(ctx) comment := ctx.Comment
if comment == nil { attachment := getIssueCommentAttachmentSafeRead(ctx)
return
}
attachment := getIssueCommentAttachmentSafeRead(ctx, comment)
if attachment == nil { if attachment == nil {
return return
} }
@ -101,10 +98,7 @@ func ListIssueCommentAttachments(ctx *context.APIContext) {
// "$ref": "#/responses/AttachmentList" // "$ref": "#/responses/AttachmentList"
// "404": // "404":
// "$ref": "#/responses/error" // "$ref": "#/responses/error"
comment := getIssueCommentSafe(ctx) comment := ctx.Comment
if comment == nil {
return
}
if err := comment.LoadAttachments(ctx); err != nil { if err := comment.LoadAttachments(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err) ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
@ -166,14 +160,12 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
// "$ref": "#/responses/repoArchivedError" // "$ref": "#/responses/repoArchivedError"
// Check if comment exists and load comment // Check if comment exists and load comment
comment := getIssueCommentSafe(ctx)
if comment == nil { if !canUserWriteIssueCommentAttachment(ctx) {
return return
} }
if !canUserWriteIssueCommentAttachment(ctx, comment) { comment := ctx.Comment
return
}
updatedAt := ctx.Req.FormValue("updated_at") updatedAt := ctx.Req.FormValue("updated_at")
if len(updatedAt) != 0 { if len(updatedAt) != 0 {
@ -341,42 +333,17 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) {
ctx.Status(http.StatusNoContent) ctx.Status(http.StatusNoContent)
} }
func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64("id"))
if err != nil {
ctx.NotFoundOrServerError("GetCommentByID", issues_model.IsErrCommentNotExist, err)
return nil
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
return nil
}
if comment.Issue == nil || comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.Error(http.StatusNotFound, "", "no matching issue comment found")
return nil
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
return nil
}
comment.Issue.Repo = ctx.Repo.Repository
return comment
}
func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment { func getIssueCommentAttachmentSafeWrite(ctx *context.APIContext) *repo_model.Attachment {
comment := getIssueCommentSafe(ctx) if !canUserWriteIssueCommentAttachment(ctx) {
if comment == nil {
return nil return nil
} }
if !canUserWriteIssueCommentAttachment(ctx, comment) { return getIssueCommentAttachmentSafeRead(ctx)
return nil
}
return getIssueCommentAttachmentSafeRead(ctx, comment)
} }
func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues_model.Comment) bool { func canUserWriteIssueCommentAttachment(ctx *context.APIContext) bool {
// ctx.Comment is assumed to be set in a safe way via a middleware
comment := ctx.Comment
canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) canEditComment := ctx.IsSigned && (ctx.Doer.ID == comment.PosterID || ctx.IsUserRepoAdmin() || ctx.IsUserSiteAdmin()) && ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)
if !canEditComment { if !canEditComment {
ctx.Error(http.StatusForbidden, "", "user should have permission to edit comment") ctx.Error(http.StatusForbidden, "", "user should have permission to edit comment")
@ -386,7 +353,10 @@ func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues
return true return true
} }
func getIssueCommentAttachmentSafeRead(ctx *context.APIContext, comment *issues_model.Comment) *repo_model.Attachment { func getIssueCommentAttachmentSafeRead(ctx *context.APIContext) *repo_model.Attachment {
// ctx.Comment is assumed to be set in a safe way via a middleware
comment := ctx.Comment
attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("attachment_id")) attachment, err := repo_model.GetAttachmentByID(ctx, ctx.ParamsInt64("attachment_id"))
if err != nil { if err != nil {
ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err) ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err)

View file

@ -51,30 +51,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
// "404": // "404":
// "$ref": "#/responses/notFound" // "$ref": "#/responses/notFound"
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) comment := ctx.Comment
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
}
return
}
if err := comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound()
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.Error(http.StatusForbidden, "GetIssueCommentReactions", errors.New("no permission to get reactions"))
return
}
reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID) reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID)
if err != nil { if err != nil {
@ -188,30 +165,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) {
} }
func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) { func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id")) comment := ctx.Comment
if err != nil {
if issues_model.IsErrCommentNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetCommentByID", err)
}
return
}
if err = comment.LoadIssue(ctx); err != nil {
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err)
return
}
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
ctx.NotFound()
return
}
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
ctx.NotFound()
return
}
if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) { if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
ctx.Error(http.StatusForbidden, "ChangeIssueCommentReaction", errors.New("no permission to change reaction")) ctx.Error(http.StatusForbidden, "ChangeIssueCommentReaction", errors.New("no permission to change reaction"))
@ -243,7 +197,7 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
}) })
} else { } else {
// DeleteIssueCommentReaction part // DeleteIssueCommentReaction part
err = issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction) err := issues_model.DeleteCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteCommentReaction", err) ctx.Error(http.StatusInternalServerError, "DeleteCommentReaction", err)
return return

View file

@ -208,6 +208,160 @@ func GetPullReviewComments(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, apiComments) ctx.JSON(http.StatusOK, apiComments)
} }
// GetPullReviewComment get a pull review comment
func GetPullReviewComment(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment} repository repoGetPullReviewComment
// ---
// summary: Get a pull review comment
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the pull request
// type: integer
// format: int64
// required: true
// - name: id
// in: path
// description: id of the review
// type: integer
// format: int64
// required: true
// - name: comment
// in: path
// description: id of the comment
// type: integer
// format: int64
// required: true
// responses:
// "200":
// "$ref": "#/responses/PullReviewComment"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
review, _, statusSet := prepareSingleReview(ctx)
if statusSet {
return
}
if err := ctx.Comment.LoadPoster(ctx); err != nil {
ctx.InternalServerError(err)
return
}
apiComment, err := convert.ToPullReviewComment(ctx, review, ctx.Comment, ctx.Doer)
if err != nil {
ctx.InternalServerError(err)
return
}
ctx.JSON(http.StatusOK, apiComment)
}
// CreatePullReviewComments add a new comment to a pull request review
func CreatePullReviewComment(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments repository repoCreatePullReviewComment
// ---
// summary: Add a new comment to a pull request review
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the pull request
// type: integer
// format: int64
// required: true
// - name: id
// in: path
// description: id of the review
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/CreatePullReviewCommentOptions"
// responses:
// "200":
// "$ref": "#/responses/PullReviewComment"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.CreatePullReviewCommentOptions)
review, pr, statusSet := prepareSingleReview(ctx)
if statusSet {
return
}
if err := pr.Issue.LoadRepo(ctx); err != nil {
ctx.InternalServerError(err)
return
}
line := opts.NewLineNum
if opts.OldLineNum > 0 {
line = opts.OldLineNum * -1
}
comment, err := pull_service.CreateCodeComment(ctx,
ctx.Doer,
ctx.Repo.GitRepo,
pr.Issue,
line,
opts.Body,
opts.Path,
// as of e522e774cae2240279fc48c349fc513c9d3353ee
// isPending is not needed because review.ID is always available
// and does not need to be discovered implicitly
false,
review.ID,
// as of e522e774cae2240279fc48c349fc513c9d3353ee
// latestCommitID is not needed because it is only used to
// create a new review in case it does not already exist
"",
)
if err != nil {
ctx.InternalServerError(err)
return
}
apiComment, err := convert.ToPullReviewComment(ctx, review, comment, ctx.Doer)
if err != nil {
ctx.InternalServerError(err)
return
}
ctx.JSON(http.StatusOK, apiComment)
}
// DeletePullReview delete a specific review from a pull request // DeletePullReview delete a specific review from a pull request
func DeletePullReview(ctx *context.APIContext) { func DeletePullReview(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview // swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview

View file

@ -17,6 +17,9 @@ type swaggerParameterBodies struct {
// in:body // in:body
AddCollaboratorOption api.AddCollaboratorOption AddCollaboratorOption api.AddCollaboratorOption
// in:body
ReplaceFlagsOption api.ReplaceFlagsOption
// in:body // in:body
CreateEmailOption api.CreateEmailOption CreateEmailOption api.CreateEmailOption
// in:body // in:body
@ -158,6 +161,9 @@ type swaggerParameterBodies struct {
// in:body // in:body
CreatePullReviewComment api.CreatePullReviewComment CreatePullReviewComment api.CreatePullReviewComment
// in:body
CreatePullReviewCommentOptions api.CreatePullReviewCommentOptions
// in:body // in:body
SubmitPullReviewOptions api.SubmitPullReviewOptions SubmitPullReviewOptions api.SubmitPullReviewOptions

View file

@ -358,6 +358,12 @@ func SubmitInstall(ctx *context.Context) {
ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form) ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplInstall, form)
return return
} }
if len(form.AdminPasswd) < setting.MinPasswordLength {
ctx.Data["Err_Admin"] = true
ctx.Data["Err_AdminPasswd"] = true
ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplInstall, form)
return
}
} }
// Init the engine with migration // Init the engine with migration

View file

@ -32,6 +32,7 @@ import (
"code.gitea.io/gitea/services/externalaccount" "code.gitea.io/gitea/services/externalaccount"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/mailer" "code.gitea.io/gitea/services/mailer"
notify_service "code.gitea.io/gitea/services/notify"
"github.com/markbates/goth" "github.com/markbates/goth"
) )
@ -600,6 +601,7 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
} }
} }
notify_service.NewUserSignUp(ctx, u)
// update external user information // update external user information
if gothUser != nil { if gothUser != nil {
if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil { if err := externalaccount.UpdateExternalUser(ctx, u, *gothUser); err != nil {
@ -645,13 +647,22 @@ func Activate(ctx *context.Context) {
} }
// Resend confirmation email. // Resend confirmation email.
if setting.Service.RegisterEmailConfirm { if setting.Service.RegisterEmailConfirm {
if ctx.Cache.IsExist("MailResendLimit_" + ctx.Doer.LowerName) { var cacheKey string
if ctx.Cache.IsExist("MailChangedJustNow_" + ctx.Doer.LowerName) {
cacheKey = "MailChangedLimit_"
if err := ctx.Cache.Delete("MailChangedJustNow_" + ctx.Doer.LowerName); err != nil {
log.Error("Delete cache(MailChangedJustNow) fail: %v", err)
}
} else {
cacheKey = "MailResendLimit_"
}
if ctx.Cache.IsExist(cacheKey + ctx.Doer.LowerName) {
ctx.Data["ResendLimited"] = true ctx.Data["ResendLimited"] = true
} else { } else {
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale) ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer) mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil { if err := ctx.Cache.Put(cacheKey+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
log.Error("Set cache(MailResendLimit) fail: %v", err) log.Error("Set cache(MailResendLimit) fail: %v", err)
} }
} }
@ -685,6 +696,43 @@ func Activate(ctx *context.Context) {
func ActivatePost(ctx *context.Context) { func ActivatePost(ctx *context.Context) {
code := ctx.FormString("code") code := ctx.FormString("code")
if len(code) == 0 { if len(code) == 0 {
email := ctx.FormString("email")
if len(email) > 0 {
ctx.Data["IsActivatePage"] = true
if ctx.Doer == nil || ctx.Doer.IsActive {
ctx.NotFound("invalid user", nil)
return
}
// Change the primary email
if setting.Service.RegisterEmailConfirm {
if ctx.Cache.IsExist("MailChangeLimit_" + ctx.Doer.LowerName) {
ctx.Data["ResendLimited"] = true
} else {
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
err := user_model.ReplaceInactivePrimaryEmail(ctx, ctx.Doer.Email, &user_model.EmailAddress{
UID: ctx.Doer.ID,
Email: email,
})
if err != nil {
ctx.Data["IsActivatePage"] = false
log.Error("Couldn't replace inactive primary email of user %d: %v", ctx.Doer.ID, err)
ctx.RenderWithErr(ctx.Tr("auth.change_unconfirmed_email_error", err), TplActivate, nil)
return
}
if err := ctx.Cache.Put("MailChangeLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
log.Error("Set cache(MailChangeLimit) fail: %v", err)
}
if err := ctx.Cache.Put("MailChangedJustNow_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
log.Error("Set cache(MailChangedJustNow) fail: %v", err)
}
// Confirmation mail will be re-sent after the redirect to `/user/activate` below.
}
} else {
ctx.Data["ServiceNotEnabled"] = true
}
}
ctx.Redirect(setting.AppSubURL + "/user/activate") ctx.Redirect(setting.AppSubURL + "/user/activate")
return return
} }

View file

@ -951,10 +951,16 @@ func SignInOAuthCallback(ctx *context.Context) {
return return
} else if !setting.Service.AllowOnlyInternalRegistration && setting.OAuth2Client.EnableAutoRegistration { } else if !setting.Service.AllowOnlyInternalRegistration && setting.OAuth2Client.EnableAutoRegistration {
// create new user with details from oauth2 provider // create new user with details from oauth2 provider
var missingFields []string
if gothUser.UserID == "" { if gothUser.UserID == "" {
missingFields = append(missingFields, "sub") log.Error("OAuth2 Provider %s returned empty or missing field: UserID", authSource.Name)
if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" {
log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields")
} }
err = fmt.Errorf("OAuth2 Provider %s returned empty or missing field: UserID", authSource.Name)
ctx.ServerError("CreateUser", err)
return
}
var missingFields []string
if gothUser.Email == "" { if gothUser.Email == "" {
missingFields = append(missingFields, "email") missingFields = append(missingFields, "email")
} }
@ -962,12 +968,10 @@ func SignInOAuthCallback(ctx *context.Context) {
missingFields = append(missingFields, "nickname") missingFields = append(missingFields, "nickname")
} }
if len(missingFields) > 0 { if len(missingFields) > 0 {
log.Error("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields) // we don't have enough information to create an account automatically,
if authSource.IsOAuth2() && authSource.Cfg.(*oauth2.Source).Provider == "openidConnect" { // so we prompt the user for the remaining bits
log.Error("You may need to change the 'OPENID_CONNECT_SCOPES' setting to request all required fields") log.Trace("OAuth2 Provider %s returned empty or missing fields: %s, prompting the user for them", authSource.Name, missingFields)
} showLinkingLogin(ctx, gothUser)
err = fmt.Errorf("OAuth2 Provider %s returned empty or missing fields: %s", authSource.Name, missingFields)
ctx.ServerError("CreateUser", err)
return return
} }
uname, err := getUserName(&gothUser) uname, err := getUserName(&gothUser)

View file

@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"github.com/gorilla/feeds" "github.com/gorilla/feeds"
"github.com/jaytaylor/html2text"
) )
func toBranchLink(ctx *context.Context, act *activities_model.Action) string { func toBranchLink(ctx *context.Context, act *activities_model.Action) string {
@ -240,8 +241,15 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
content = desc content = desc
} }
// It's a common practice for feed generators to use plain text titles.
// See https://codeberg.org/forgejo/forgejo/pulls/1595
plainTitle, err := html2text.FromString(title, html2text.Options{OmitLinks: true})
if err != nil {
return nil, err
}
items = append(items, &feeds.Item{ items = append(items, &feeds.Item{
Title: title, Title: plainTitle,
Link: link, Link: link,
Description: desc, Description: desc,
Author: &feeds.Author{ Author: &feeds.Author{

View file

@ -8,11 +8,12 @@ import (
) )
// RenderBranchFeed render format for branch or file // RenderBranchFeed render format for branch or file
func RenderBranchFeed(ctx *context.Context) { func RenderBranchFeed(feedType string) func(ctx *context.Context) {
_, _, showFeedType := GetFeedType(ctx.Params(":reponame"), ctx.Req) return func(ctx *context.Context) {
if ctx.Repo.TreePath == "" { if ctx.Repo.TreePath == "" {
ShowBranchFeed(ctx, ctx.Repo.Repository, showFeedType) ShowBranchFeed(ctx, ctx.Repo.Repository, feedType)
} else { } else {
ShowFileFeed(ctx, ctx.Repo.Repository, showFeedType) ShowFileFeed(ctx, ctx.Repo.Repository, feedType)
}
} }
} }

View file

@ -46,6 +46,20 @@ func View(ctx *context_module.Context) {
ctx.HTML(http.StatusOK, tplViewActions) ctx.HTML(http.StatusOK, tplViewActions)
} }
func ViewLatest(ctx *context_module.Context) {
run, err := actions_model.GetLatestRun(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.NotFound("GetLatestRun", err)
return
}
err = run.LoadAttributes(ctx)
if err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
ctx.Redirect(run.HTMLURL(), http.StatusTemporaryRedirect)
}
type ViewRequest struct { type ViewRequest struct {
LogCursors []struct { LogCursors []struct {
Step int `json:"step"` Step int `json:"step"`

View file

@ -0,0 +1,165 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package badges
import (
"fmt"
"net/url"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
context_module "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
)
func getBadgeURL(ctx *context_module.Context, label, text, color string) string {
sb := &strings.Builder{}
_ = setting.Badges.GeneratorURLTemplateTemplate.Execute(sb, map[string]string{
"label": url.PathEscape(label),
"text": url.PathEscape(text),
"color": url.PathEscape(color),
})
badgeURL := sb.String()
q := ctx.Req.URL.Query()
// Remove any `branch` or `event` query parameters. They're used by the
// workflow badge route, and do not need forwarding to the badge generator.
delete(q, "branch")
delete(q, "event")
if len(q) > 0 {
return fmt.Sprintf("%s?%s", badgeURL, q.Encode())
}
return badgeURL
}
func redirectToBadge(ctx *context_module.Context, label, text, color string) {
ctx.Redirect(getBadgeURL(ctx, label, text, color))
}
func errorBadge(ctx *context_module.Context, label, text string) {
ctx.Redirect(getBadgeURL(ctx, label, text, "crimson"))
}
func GetWorkflowBadge(ctx *context_module.Context) {
branch := ctx.Req.URL.Query().Get("branch")
if branch == "" {
branch = ctx.Repo.Repository.DefaultBranch
}
branch = fmt.Sprintf("refs/heads/%s", branch)
event := ctx.Req.URL.Query().Get("event")
workflowFile := ctx.Params("workflow_name")
run, err := actions_model.GetLatestRunForBranchAndWorkflow(ctx, ctx.Repo.Repository.ID, branch, workflowFile, event)
if err != nil {
errorBadge(ctx, workflowFile, "Not found")
return
}
var color string
switch run.Status {
case actions_model.StatusUnknown:
color = "lightgrey"
case actions_model.StatusWaiting:
color = "lightgrey"
case actions_model.StatusRunning:
color = "gold"
case actions_model.StatusSuccess:
color = "brightgreen"
case actions_model.StatusFailure:
color = "crimson"
case actions_model.StatusCancelled:
color = "orange"
case actions_model.StatusSkipped:
color = "blue"
case actions_model.StatusBlocked:
color = "yellow"
default:
color = "lightgrey"
}
redirectToBadge(ctx, workflowFile, run.Status.String(), color)
}
func getIssueOrPullBadge(ctx *context_module.Context, label, variant string, num int) {
var text string
if len(variant) > 0 {
text = fmt.Sprintf("%d %s", num, variant)
} else {
text = fmt.Sprintf("%d", num)
}
redirectToBadge(ctx, label, text, "blue")
}
func getIssueBadge(ctx *context_module.Context, variant string, num int) {
if !ctx.Repo.CanRead(unit.TypeIssues) &&
!ctx.Repo.CanRead(unit.TypeExternalTracker) {
errorBadge(ctx, "issues", "Not found")
return
}
_, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
if err == nil {
errorBadge(ctx, "issues", "Not found")
return
}
getIssueOrPullBadge(ctx, "issues", variant, num)
}
func getPullBadge(ctx *context_module.Context, variant string, num int) {
if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(unit.TypePullRequests) {
errorBadge(ctx, "pulls", "Not found")
return
}
getIssueOrPullBadge(ctx, "pulls", variant, num)
}
func GetOpenIssuesBadge(ctx *context_module.Context) {
getIssueBadge(ctx, "open", ctx.Repo.Repository.NumOpenIssues)
}
func GetClosedIssuesBadge(ctx *context_module.Context) {
getIssueBadge(ctx, "closed", ctx.Repo.Repository.NumClosedIssues)
}
func GetTotalIssuesBadge(ctx *context_module.Context) {
getIssueBadge(ctx, "", ctx.Repo.Repository.NumIssues)
}
func GetOpenPullsBadge(ctx *context_module.Context) {
getPullBadge(ctx, "open", ctx.Repo.Repository.NumOpenPulls)
}
func GetClosedPullsBadge(ctx *context_module.Context) {
getPullBadge(ctx, "closed", ctx.Repo.Repository.NumClosedPulls)
}
func GetTotalPullsBadge(ctx *context_module.Context) {
getPullBadge(ctx, "", ctx.Repo.Repository.NumPulls)
}
func GetStarsBadge(ctx *context_module.Context) {
redirectToBadge(ctx, "stars", fmt.Sprintf("%d", ctx.Repo.Repository.NumStars), "blue")
}
func GetLatestReleaseBadge(ctx *context_module.Context) {
release, err := repo_model.GetLatestReleaseByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
if repo_model.IsErrReleaseNotExist(err) {
errorBadge(ctx, "release", "Not found")
return
}
ctx.ServerError("GetLatestReleaseByRepoID", err)
}
if err := release.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadAttributes", err)
return
}
redirectToBadge(ctx, "release", release.TagName, "blue")
}

View file

@ -243,6 +243,22 @@ func FileHistory(ctx *context.Context) {
ctx.ServerError("CommitsByFileAndRange", err) ctx.ServerError("CommitsByFileAndRange", err)
return return
} }
oldestCommit := commits[len(commits)-1]
renamedFiles, err := git.GetCommitFileRenames(ctx, ctx.Repo.GitRepo.Path, oldestCommit.ID.String())
if err != nil {
ctx.ServerError("GetCommitFileRenames", err)
return
}
for _, renames := range renamedFiles {
if renames[1] == fileName {
ctx.Data["OldFilename"] = renames[0]
ctx.Data["OldFilenameHistory"] = fmt.Sprintf("%s/commits/commit/%s/%s", ctx.Repo.RepoLink, oldestCommit.ID.String(), renames[0])
break
}
}
ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository) ctx.Data["Commits"] = git_model.ConvertFromGitCommit(ctx, commits, ctx.Repo.Repository)
ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Username"] = ctx.Repo.Owner.Name

View file

@ -14,6 +14,7 @@ import (
git_model "code.gitea.io/gitea/models/git" git_model "code.gitea.io/gitea/models/git"
repo_model "code.gitea.io/gitea/models/repo" repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/charset"
"code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/context"
@ -99,6 +100,27 @@ func getParentTreeFields(treePath string) (treeNames, treePaths []string) {
return treeNames, treePaths return treeNames, treePaths
} }
// getSelectableEmailAddresses returns which emails can be used by the user as
// email for a Git commiter.
func getSelectableEmailAddresses(ctx *context.Context) ([]*user_model.ActivatedEmailAddress, error) {
// Retrieve emails that the user could use for commiter identity.
commitEmails, err := user_model.GetActivatedEmailAddresses(ctx, ctx.Doer.ID)
if err != nil {
return nil, fmt.Errorf("GetActivatedEmailAddresses: %w", err)
}
// Allow for the placeholder mail to be used. Use -1 as ID to identify
// this entry to be the placerholder mail of the user.
placeholderMail := &user_model.ActivatedEmailAddress{ID: -1, Email: ctx.Doer.GetPlaceholderEmail()}
if ctx.Doer.KeepEmailPrivate {
commitEmails = append([]*user_model.ActivatedEmailAddress{placeholderMail}, commitEmails...)
} else {
commitEmails = append(commitEmails, placeholderMail)
}
return commitEmails, nil
}
func editFile(ctx *context.Context, isNewFile bool) { func editFile(ctx *context.Context, isNewFile bool) {
ctx.Data["PageIsEdit"] = true ctx.Data["PageIsEdit"] = true
ctx.Data["IsNewFile"] = isNewFile ctx.Data["IsNewFile"] = isNewFile
@ -177,6 +199,12 @@ func editFile(ctx *context.Context, isNewFile bool) {
treeNames = append(treeNames, fileName) treeNames = append(treeNames, fileName)
} }
commitEmails, err := getSelectableEmailAddresses(ctx)
if err != nil {
ctx.ServerError("getSelectableEmailAddresses", err)
return
}
ctx.Data["TreeNames"] = treeNames ctx.Data["TreeNames"] = treeNames
ctx.Data["TreePaths"] = treePaths ctx.Data["TreePaths"] = treePaths
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
@ -192,6 +220,8 @@ func editFile(ctx *context.Context, isNewFile bool) {
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath) ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, treePath)
ctx.Data["CommitMails"] = commitEmails
ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail()
ctx.HTML(http.StatusOK, tplEditFile) ctx.HTML(http.StatusOK, tplEditFile)
} }
@ -227,6 +257,12 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
branchName = form.NewBranchName branchName = form.NewBranchName
} }
commitEmails, err := getSelectableEmailAddresses(ctx)
if err != nil {
ctx.ServerError("getSelectableEmailAddresses", err)
return
}
ctx.Data["PageIsEdit"] = true ctx.Data["PageIsEdit"] = true
ctx.Data["PageHasPosted"] = true ctx.Data["PageHasPosted"] = true
ctx.Data["IsNewFile"] = isNewFile ctx.Data["IsNewFile"] = isNewFile
@ -243,6 +279,8 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",") ctx.Data["PreviewableExtensions"] = strings.Join(markup.PreviewableExtensions(), ",")
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",") ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath) ctx.Data["EditorconfigJson"] = GetEditorConfig(ctx, form.TreePath)
ctx.Data["CommitMails"] = commitEmails
ctx.Data["DefaultCommitMail"] = ctx.Doer.GetEmail()
if ctx.HasError() { if ctx.HasError() {
ctx.HTML(http.StatusOK, tplEditFile) ctx.HTML(http.StatusOK, tplEditFile)
@ -277,6 +315,30 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
operation = "create" operation = "create"
} }
gitIdentity := &files_service.IdentityOptions{
Name: ctx.Doer.Name,
}
// -1 is defined as placeholder email.
if form.CommitMailID == -1 {
gitIdentity.Email = ctx.Doer.GetPlaceholderEmail()
} else {
// Check if the given email is activated.
email, err := user_model.GetEmailAddressByID(ctx, ctx.Doer.ID, form.CommitMailID)
if err != nil {
ctx.ServerError("GetEmailAddressByID", err)
return
}
if email == nil || !email.IsActivated {
ctx.Data["Err_CommitMailID"] = true
ctx.RenderWithErr(ctx.Tr("repo.editor.invalid_commit_mail"), tplEditFile, &form)
return
}
gitIdentity.Email = email.Email
}
if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{ if _, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, &files_service.ChangeRepoFilesOptions{
LastCommitID: form.LastCommit, LastCommitID: form.LastCommit,
OldBranch: ctx.Repo.BranchName, OldBranch: ctx.Repo.BranchName,
@ -291,6 +353,8 @@ func editFilePost(ctx *context.Context, form forms.EditRepoFileForm, isNewFile b
}, },
}, },
Signoff: form.Signoff, Signoff: form.Signoff,
Author: gitIdentity,
Committer: gitIdentity,
}); err != nil { }); err != nil {
// This is where we handle all the errors thrown by files_service.ChangeRepoFiles // This is where we handle all the errors thrown by files_service.ChangeRepoFiles
if git.IsErrNotExist(err) { if git.IsErrNotExist(err) {

View file

@ -0,0 +1,49 @@
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
// SPDX-License-Identifier: MIT
package flags
import (
"net/http"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
)
const (
tplRepoFlags base.TplName = "repo/flags"
)
func Manage(ctx *context.Context) {
ctx.Data["IsRepoFlagsPage"] = true
ctx.Data["Title"] = ctx.Tr("repo.admin.manage_flags")
flags := map[string]bool{}
for _, f := range setting.Repository.SettableFlags {
flags[f] = false
}
repoFlags, _ := ctx.Repo.Repository.ListFlags(ctx)
for _, f := range repoFlags {
flags[f.Name] = true
}
ctx.Data["Flags"] = flags
ctx.HTML(http.StatusOK, tplRepoFlags)
}
func ManagePost(ctx *context.Context) {
newFlags := ctx.FormStrings("flags")
err := ctx.Repo.Repository.ReplaceAllFlags(ctx, newFlags)
if err != nil {
ctx.Flash.Error(ctx.Tr("repo.admin.failed_to_replace_flags"))
log.Error("Error replacing repository flags for repo %d: %v", ctx.Repo.Repository.ID, err)
} else {
ctx.Flash.Success(ctx.Tr("repo.admin.flags_replaced"))
}
ctx.Redirect(ctx.Repo.Repository.HTMLURL() + "/flags")
}

View file

@ -2488,7 +2488,8 @@ func UpdatePullReviewRequest(ctx *context.Context) {
func SearchIssues(ctx *context.Context) { func SearchIssues(ctx *context.Context) {
before, since, err := context.GetQueryBeforeSince(ctx.Base) before, since, err := context.GetQueryBeforeSince(ctx.Base)
if err != nil { if err != nil {
ctx.Error(http.StatusUnprocessableEntity, err.Error()) log.Error("GetQueryBeforeSince: %v", err)
ctx.Error(http.StatusUnprocessableEntity, "invalid before or since")
return return
} }
@ -2525,10 +2526,11 @@ func SearchIssues(ctx *context.Context) {
if ctx.FormString("owner") != "" { if ctx.FormString("owner") != "" {
owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner")) owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
if err != nil { if err != nil {
log.Error("GetUserByName: %v", err)
if user_model.IsErrUserNotExist(err) { if user_model.IsErrUserNotExist(err) {
ctx.Error(http.StatusBadRequest, "Owner not found", err.Error()) ctx.Error(http.StatusBadRequest, "Owner not found", err.Error())
} else { } else {
ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error()) ctx.Error(http.StatusInternalServerError)
} }
return return
} }
@ -2539,15 +2541,16 @@ func SearchIssues(ctx *context.Context) {
} }
if ctx.FormString("team") != "" { if ctx.FormString("team") != "" {
if ctx.FormString("owner") == "" { if ctx.FormString("owner") == "" {
ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team") ctx.Error(http.StatusBadRequest, "Owner organisation is required for filtering on team")
return return
} }
team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team")) team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
if err != nil { if err != nil {
log.Error("GetTeam: %v", err)
if organization.IsErrTeamNotExist(err) { if organization.IsErrTeamNotExist(err) {
ctx.Error(http.StatusBadRequest, "Team not found", err.Error()) ctx.Error(http.StatusBadRequest)
} else { } else {
ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error()) ctx.Error(http.StatusInternalServerError)
} }
return return
} }
@ -2560,7 +2563,8 @@ func SearchIssues(ctx *context.Context) {
} }
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts) repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error()) log.Error("SearchRepositoryIDs: %v", err)
ctx.Error(http.StatusInternalServerError)
return return
} }
if len(repoIDs) == 0 { if len(repoIDs) == 0 {
@ -2594,7 +2598,8 @@ func SearchIssues(ctx *context.Context) {
} }
includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames) includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error()) log.Error("GetLabelIDsByNames: %v", err)
ctx.Error(http.StatusInternalServerError)
return return
} }
} }
@ -2608,7 +2613,8 @@ func SearchIssues(ctx *context.Context) {
} }
includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames) includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error()) log.Error("GetMilestoneIDsByNames: %v", err)
ctx.Error(http.StatusInternalServerError)
return return
} }
} }
@ -2675,12 +2681,14 @@ func SearchIssues(ctx *context.Context) {
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "SearchIssues", err.Error()) log.Error("SearchIssues: %v", err)
ctx.Error(http.StatusInternalServerError)
return return
} }
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true) issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
if err != nil { if err != nil {
ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err.Error()) log.Error("GetIssuesByIDs: %v", err)
ctx.Error(http.StatusInternalServerError)
return return
} }

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