Merge branch 'rebase-forgejo-dependency' into wip-forgejo
This commit is contained in:
commit
b0a7fe95ad
249 changed files with 7980 additions and 856 deletions
|
@ -100,6 +100,8 @@ package "code.gitea.io/gitea/models/unittest"
|
|||
func LoadFixtures
|
||||
func Copy
|
||||
func CopyDir
|
||||
func NewMockWebServer
|
||||
func NormalizedFullPath
|
||||
func FixturesDir
|
||||
func fatalTestError
|
||||
func InitSettings
|
||||
|
@ -322,6 +324,7 @@ package "code.gitea.io/gitea/services/pull"
|
|||
package "code.gitea.io/gitea/services/repository"
|
||||
func GetBranchCommitID
|
||||
func IsErrForkAlreadyExist
|
||||
func UpdateRepositoryUnits
|
||||
|
||||
package "code.gitea.io/gitea/services/repository/archiver"
|
||||
func ArchiveRepository
|
||||
|
|
10
assets/go-licenses.json
generated
10
assets/go-licenses.json
generated
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
"go.buildTags": "'sqlite sqlite_unlock_notify'",
|
||||
"go.buildTags": "sqlite,sqlite_unlock_notify",
|
||||
"go.testFlags": ["-v"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -412,6 +412,10 @@ USER = root
|
|||
;;
|
||||
;; Whether execute database models migrations automatically
|
||||
;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
|
||||
;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"
|
||||
;; Limited is for users visible only to signed users
|
||||
;; 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]
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -1470,6 +1487,8 @@ LEVEL = Info
|
|||
;;
|
||||
;; Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
||||
;DEFAULT_EMAIL_NOTIFICATIONS = enabled
|
||||
;; Send an email to all admins when a new user signs up to inform the admins about this act. Options: true, false
|
||||
;SEND_NOTIFICATION_EMAIL_ON_NEW_USER = false
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -1783,9 +1802,6 @@ LEVEL = Info
|
|||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;AVATAR_UPLOAD_PATH = data/avatars
|
||||
;REPOSITORY_AVATAR_UPLOAD_PATH = data/repo-avatars
|
||||
;;
|
||||
;; How Gitea deals with missing repository avatars
|
||||
;; none = no avatar will be displayed; random = random avatar will be displayed; image = default image will be used
|
||||
;REPOSITORY_AVATAR_FALLBACK = none
|
||||
|
|
|
@ -458,6 +458,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
|
|||
- `MAX_IDLE_CONNS` **2**: Max idle database connections on connection pool, default is 2 - this will be capped to `MAX_OPEN_CONNS`.
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
|
@ -517,6 +518,7 @@ And the following unique queues:
|
|||
|
||||
- `DEFAULT_EMAIL_NOTIFICATIONS`: **enabled**: Default configuration for email notifications for users (user configurable). Options: enabled, onmention, disabled
|
||||
- `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`)
|
||||
|
||||
|
|
8
go.mod
8
go.mod
|
@ -15,7 +15,6 @@ require (
|
|||
gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4
|
||||
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
|
||||
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/alecthomas/chroma/v2 v2.12.0
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb
|
||||
|
@ -77,14 +76,12 @@ require (
|
|||
github.com/mholt/archiver/v3 v3.5.1
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
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/nektos/act v0.2.52
|
||||
github.com/niklasfasching/go-org v1.7.0
|
||||
github.com/olivere/elastic/v7 v7.0.32
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
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/prometheus/client_golang v1.17.0
|
||||
github.com/quasoft/websspi v1.1.2
|
||||
|
@ -100,7 +97,6 @@ require (
|
|||
github.com/ulikunitz/xz v0.5.11
|
||||
github.com/urfave/cli/v2 v2.26.0
|
||||
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/yuin/goldmark v1.6.0
|
||||
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/miekg/dns v1.1.57 // 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/mapstructure v1.5.0 // 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/pierrec/lz4/v4 v4.1.19 // 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/prometheus/client_model v0.5.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/x448/float16 v0.8.4 // 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/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||
|
|
9
go.sum
9
go.sum
|
@ -93,8 +93,6 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa
|
|||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
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/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM=
|
||||
|
@ -837,13 +835,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/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
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/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
|
||||
|
|
|
@ -171,14 +171,13 @@ func updateRepoRunsNumbers(ctx context.Context, repo *repo_model.Repository) err
|
|||
}
|
||||
|
||||
// CancelRunningJobs cancels all running and waiting jobs associated with a specific workflow.
|
||||
func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) error {
|
||||
func CancelRunningJobs(ctx context.Context, repoID int64, ref, workflowID string) error {
|
||||
// Find all runs in the specified repository, reference, and workflow with statuses 'Running' or 'Waiting'.
|
||||
runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
|
||||
RepoID: repoID,
|
||||
Ref: ref,
|
||||
WorkflowID: workflowID,
|
||||
TriggerEvent: event,
|
||||
Status: []Status{StatusRunning, StatusWaiting},
|
||||
RepoID: repoID,
|
||||
Ref: ref,
|
||||
WorkflowID: workflowID,
|
||||
Status: []Status{StatusRunning, StatusWaiting},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -312,6 +311,32 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
|
|||
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) {
|
||||
var run ActionRun
|
||||
has, err := db.GetEngine(ctx).Where("id=?", id).Get(&run)
|
||||
|
|
|
@ -10,7 +10,6 @@ import (
|
|||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
@ -72,7 +71,6 @@ type FindRunOptions struct {
|
|||
WorkflowID string
|
||||
Ref string // the commit/tag/… that caused this workflow
|
||||
TriggerUserID int64
|
||||
TriggerEvent webhook_module.HookEventType
|
||||
Approved bool // not util.OptionalBool, it works only when it's true
|
||||
Status []Status
|
||||
}
|
||||
|
@ -100,9 +98,6 @@ func (opts FindRunOptions) ToConds() builder.Cond {
|
|||
if opts.Ref != "" {
|
||||
cond = cond.And(builder.Eq{"ref": opts.Ref})
|
||||
}
|
||||
if opts.TriggerEvent != "" {
|
||||
cond = cond.And(builder.Eq{"trigger_event": opts.TriggerEvent})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ package actions
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -119,22 +118,3 @@ func DeleteScheduleTaskByRepo(ctx context.Context, id int64) error {
|
|||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func CleanRepoScheduleTasks(ctx context.Context, repo *repo_model.Repository) error {
|
||||
// If actions disabled when there is schedule task, this will remove the outdated schedule tasks
|
||||
// There is no other place we can do this because the app.ini will be changed manually
|
||||
if err := DeleteScheduleTaskByRepo(ctx, repo.ID); err != nil {
|
||||
return fmt.Errorf("DeleteCronTaskByRepo: %v", err)
|
||||
}
|
||||
// cancel running cron jobs of this repository and delete old schedules
|
||||
if err := CancelRunningJobs(
|
||||
ctx,
|
||||
repo.ID,
|
||||
repo.DefaultBranch,
|
||||
"",
|
||||
webhook_module.HookEventSchedule,
|
||||
); err != nil {
|
||||
return fmt.Errorf("CancelRunningJobs: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ func TestMain(m *testing.M) {
|
|||
FixtureFiles: []string{
|
||||
"gpg_key.yml",
|
||||
"public_key.yml",
|
||||
"TestParseCommitWithSSHSignature/public_key.yml",
|
||||
"deploy_key.yml",
|
||||
"gpg_key_import.yml",
|
||||
"user.yml",
|
||||
|
|
|
@ -169,7 +169,12 @@ func RewriteAllPublicKeys(ctx context.Context) error {
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -92,7 +92,12 @@ func RewriteAllPrincipalKeys(ctx context.Context) error {
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,12 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
|
|||
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
|
||||
for _, e := range committerEmailAddresses {
|
||||
if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
|
||||
|
|
146
models/asymkey/ssh_key_commit_verification_test.go
Normal file
146
models/asymkey/ssh_key_commit_verification_test.go
Normal file
|
@ -0,0 +1,146 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package asymkey
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseCommitWithSSHSignature(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
sshKey := unittest.AssertExistsAndLoadBean(t, &PublicKey{ID: 1000, OwnerID: 2})
|
||||
|
||||
t.Run("No commiter", func(t *testing.T) {
|
||||
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{}, &user_model.User{})
|
||||
assert.False(t, commitVerification.Verified)
|
||||
assert.Equal(t, NoKeyFound, commitVerification.Reason)
|
||||
})
|
||||
|
||||
t.Run("Commiter without keys", func(t *testing.T) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
|
||||
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, &git.Commit{Committer: &git.Signature{Email: user.Email}}, user)
|
||||
assert.False(t, commitVerification.Verified)
|
||||
assert.Equal(t, NoKeyFound, commitVerification.Reason)
|
||||
})
|
||||
|
||||
t.Run("Correct signature with wrong email", func(t *testing.T) {
|
||||
gitCommit := &git.Commit{
|
||||
Committer: &git.Signature{
|
||||
Email: "non-existent",
|
||||
},
|
||||
Signature: &git.CommitGPGSignature{
|
||||
Payload: `tree 2d491b2985a7ff848d5c02748e7ea9f9f7619f9f
|
||||
parent 45b03601635a1f463b81963a4022c7f87ce96ef9
|
||||
author user2 <non-existent> 1699710556 +0100
|
||||
committer user2 <non-existent> 1699710556 +0100
|
||||
|
||||
Using email that isn't known to Forgejo
|
||||
`,
|
||||
Signature: `-----BEGIN SSH SIGNATURE-----
|
||||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95
|
||||
f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||
AAAAQIMufOuSjZeDUujrkVK4sl7ICa0WwEftas8UAYxx0Thdkiw2qWjR1U1PKfTLm16/w8
|
||||
/bS1LX1lZNuzm2LR2qEgw=
|
||||
-----END SSH SIGNATURE-----
|
||||
`,
|
||||
},
|
||||
}
|
||||
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
|
||||
assert.False(t, commitVerification.Verified)
|
||||
assert.Equal(t, NoKeyFound, commitVerification.Reason)
|
||||
})
|
||||
|
||||
t.Run("Incorrect signature with correct email", func(t *testing.T) {
|
||||
gitCommit := &git.Commit{
|
||||
Committer: &git.Signature{
|
||||
Email: "user2@example.com",
|
||||
},
|
||||
Signature: &git.CommitGPGSignature{
|
||||
Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f
|
||||
parent c2780d5c313da2a947eae22efd7dacf4213f4e7f
|
||||
author user2 <user2@example.com> 1699707877 +0100
|
||||
committer user2 <user2@example.com> 1699707877 +0100
|
||||
|
||||
Add content
|
||||
`,
|
||||
Signature: `-----BEGIN SSH SIGNATURE-----`,
|
||||
},
|
||||
}
|
||||
|
||||
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
|
||||
assert.False(t, commitVerification.Verified)
|
||||
assert.Equal(t, NoKeyFound, commitVerification.Reason)
|
||||
})
|
||||
|
||||
t.Run("Valid signature with correct email", func(t *testing.T) {
|
||||
gitCommit := &git.Commit{
|
||||
Committer: &git.Signature{
|
||||
Email: "user2@example.com",
|
||||
},
|
||||
Signature: &git.CommitGPGSignature{
|
||||
Payload: `tree 853694aae8816094a0d875fee7ea26278dbf5d0f
|
||||
parent c2780d5c313da2a947eae22efd7dacf4213f4e7f
|
||||
author user2 <user2@example.com> 1699707877 +0100
|
||||
committer user2 <user2@example.com> 1699707877 +0100
|
||||
|
||||
Add content
|
||||
`,
|
||||
Signature: `-----BEGIN SSH SIGNATURE-----
|
||||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95
|
||||
f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||
AAAAQBe2Fwk/FKY3SBCnG6jSYcO6ucyahp2SpQ/0P+otslzIHpWNW8cQ0fGLdhhaFynJXQ
|
||||
fs9cMpZVM9BfIKNUSO8QY=
|
||||
-----END SSH SIGNATURE-----
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
|
||||
assert.True(t, commitVerification.Verified)
|
||||
assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason)
|
||||
assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
|
||||
})
|
||||
|
||||
t.Run("Valid signature with noreply email", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.NoReplyAddress, "noreply.example.com")()
|
||||
|
||||
gitCommit := &git.Commit{
|
||||
Committer: &git.Signature{
|
||||
Email: "user2@noreply.example.com",
|
||||
},
|
||||
Signature: &git.CommitGPGSignature{
|
||||
Payload: `tree 4836c7f639f37388bab4050ef5c97bbbd54272fc
|
||||
parent 795be1b0117ea5c65456050bb9fd84744d4fd9c6
|
||||
author user2 <user2@noreply.example.com> 1699709594 +0100
|
||||
committer user2 <user2@noreply.example.com> 1699709594 +0100
|
||||
|
||||
Commit with noreply
|
||||
`,
|
||||
Signature: `-----BEGIN SSH SIGNATURE-----
|
||||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgoGSe9Zy7Ez9bSJcaTNjh/Y7p95
|
||||
f5DujjqkpzFRtw6CEAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||
AAAAQJz83KKxD6Bz/ZvNpqkA3RPOSQ4LQ5FfEItbtoONkbwV9wAWMnmBqgggo/lnXCJ3oq
|
||||
muPLbvEduU+Ze/1Ol1pgk=
|
||||
-----END SSH SIGNATURE-----
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
commitVerification := ParseCommitWithSSHSignature(db.DefaultContext, gitCommit, user2)
|
||||
assert.True(t, commitVerification.Verified)
|
||||
assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason)
|
||||
assert.Equal(t, sshKey, commitVerification.SigningSSHKey)
|
||||
})
|
||||
}
|
|
@ -250,7 +250,7 @@ func (s AccessTokenScope) parse() (accessTokenScopeBitmap, error) {
|
|||
remainingScopes = remainingScopes[i+1:]
|
||||
}
|
||||
singleScope := AccessTokenScope(v)
|
||||
if singleScope == "" {
|
||||
if singleScope == "" || singleScope == "sudo" {
|
||||
continue
|
||||
}
|
||||
if singleScope == AccessTokenScopeAll {
|
||||
|
|
|
@ -20,7 +20,7 @@ func TestAccessTokenScope_Normalize(t *testing.T) {
|
|||
tests := []scopeTestNormalize{
|
||||
{"", "", 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,public-only", "public-only,all", nil},
|
||||
}
|
||||
|
|
142
models/auth/session_test.go
Normal file
142
models/auth/session_test.go
Normal file
|
@ -0,0 +1,142 @@
|
|||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAuthSession(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
defer timeutil.MockUnset()
|
||||
|
||||
key := "I-Like-Free-Software"
|
||||
|
||||
t.Run("Create Session", func(t *testing.T) {
|
||||
// Ensure it doesn't exist.
|
||||
ok, err := auth.ExistSession(db.DefaultContext, key)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
|
||||
preCount, err := auth.CountSessions(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
|
||||
now := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
timeutil.MockSet(now)
|
||||
|
||||
// New session is created.
|
||||
sess, err := auth.ReadSession(db.DefaultContext, key)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, key, sess.Key)
|
||||
assert.Empty(t, sess.Data)
|
||||
assert.EqualValues(t, now.Unix(), sess.Expiry)
|
||||
|
||||
// Ensure it exists.
|
||||
ok, err = auth.ExistSession(db.DefaultContext, key)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
|
||||
// Ensure the session is taken into account for count..
|
||||
postCount, err := auth.CountSessions(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Greater(t, postCount, preCount)
|
||||
})
|
||||
|
||||
t.Run("Update session", func(t *testing.T) {
|
||||
data := []byte{0xba, 0xdd, 0xc0, 0xde}
|
||||
now := time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
timeutil.MockSet(now)
|
||||
|
||||
// Update session.
|
||||
err := auth.UpdateSession(db.DefaultContext, key, data)
|
||||
assert.NoError(t, err)
|
||||
|
||||
timeutil.MockSet(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
// Read updated session.
|
||||
// Ensure data is updated and expiry is set from the update session call.
|
||||
sess, err := auth.ReadSession(db.DefaultContext, key)
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, key, sess.Key)
|
||||
assert.EqualValues(t, data, sess.Data)
|
||||
assert.EqualValues(t, now.Unix(), sess.Expiry)
|
||||
|
||||
timeutil.MockSet(now)
|
||||
})
|
||||
|
||||
t.Run("Delete session", func(t *testing.T) {
|
||||
// Ensure it't exist.
|
||||
ok, err := auth.ExistSession(db.DefaultContext, key)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
|
||||
preCount, err := auth.CountSessions(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = auth.DestroySession(db.DefaultContext, key)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Ensure it doens't exists.
|
||||
ok, err = auth.ExistSession(db.DefaultContext, key)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
|
||||
// Ensure the session is taken into account for count..
|
||||
postCount, err := auth.CountSessions(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Less(t, postCount, preCount)
|
||||
})
|
||||
|
||||
t.Run("Cleanup sessions", func(t *testing.T) {
|
||||
timeutil.MockSet(time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
|
||||
_, err := auth.ReadSession(db.DefaultContext, "sess-1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// One minute later.
|
||||
timeutil.MockSet(time.Date(2023, 1, 1, 0, 1, 0, 0, time.UTC))
|
||||
_, err = auth.ReadSession(db.DefaultContext, "sess-2")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// 5 minutes, shouldn't clean up anything.
|
||||
err = auth.CleanupSessions(db.DefaultContext, 5*60)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ok, err := auth.ExistSession(db.DefaultContext, "sess-1")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
|
||||
ok, err = auth.ExistSession(db.DefaultContext, "sess-2")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
|
||||
// 1 minute, should clean up sess-1.
|
||||
err = auth.CleanupSessions(db.DefaultContext, 60)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ok, err = auth.ExistSession(db.DefaultContext, "sess-1")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
|
||||
ok, err = auth.ExistSession(db.DefaultContext, "sess-2")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, ok)
|
||||
|
||||
// Now, should clean up sess-2.
|
||||
err = auth.CleanupSessions(db.DefaultContext, 0)
|
||||
assert.NoError(t, err)
|
||||
|
||||
ok, err = auth.ExistSession(db.DefaultContext, "sess-2")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, ok)
|
||||
})
|
||||
}
|
|
@ -6,6 +6,7 @@ package auth
|
|||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base32"
|
||||
"encoding/base64"
|
||||
|
@ -18,7 +19,6 @@ import (
|
|||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
|
|
@ -11,10 +11,13 @@ import (
|
|||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/contexts"
|
||||
"xorm.io/xorm/names"
|
||||
"xorm.io/xorm/schemas"
|
||||
|
||||
|
@ -144,6 +147,16 @@ func InitEngine(ctx context.Context) error {
|
|||
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
|
@ -299,3 +312,38 @@ func SetLogSQL(ctx context.Context, on bool) {
|
|||
sess.Engine().ShowSQL(on)
|
||||
}
|
||||
}
|
||||
|
||||
type SlowQueryHook struct {
|
||||
Treshold time.Duration
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
var _ contexts.Hook = &SlowQueryHook{}
|
||||
|
||||
func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
return c.Ctx, nil
|
||||
}
|
||||
|
||||
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.ExecuteTime >= h.Treshold {
|
||||
h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ErrorQueryHook struct {
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
var _ contexts.Hook = &ErrorQueryHook{}
|
||||
|
||||
func (ErrorQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
return c.Ctx, nil
|
||||
}
|
||||
|
||||
func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.Err != nil {
|
||||
h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -6,15 +6,19 @@ package db_test
|
|||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
_ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func TestDumpDatabase(t *testing.T) {
|
||||
|
@ -85,3 +89,65 @@ func TestPrimaryKeys(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlowQuery(t *testing.T) {
|
||||
lc, cleanup := test.NewLogChecker("slow-query")
|
||||
lc.StopMark("[Slow SQL Query]")
|
||||
defer cleanup()
|
||||
|
||||
e := db.GetEngine(db.DefaultContext)
|
||||
engine, ok := e.(*xorm.Engine)
|
||||
assert.True(t, ok)
|
||||
|
||||
// It's not possible to clean this up with XORM, but it's luckily not harmful
|
||||
// to leave around.
|
||||
engine.AddHook(&db.SlowQueryHook{
|
||||
Treshold: time.Second * 10,
|
||||
Logger: log.GetLogger("slow-query"),
|
||||
})
|
||||
|
||||
// NOOP query.
|
||||
e.Exec("SELECT 1 WHERE false;")
|
||||
|
||||
_, stopped := lc.Check(100 * time.Millisecond)
|
||||
assert.False(t, stopped)
|
||||
|
||||
engine.AddHook(&db.SlowQueryHook{
|
||||
Treshold: 0, // Every query should be logged.
|
||||
Logger: log.GetLogger("slow-query"),
|
||||
})
|
||||
|
||||
// NOOP query.
|
||||
e.Exec("SELECT 1 WHERE false;")
|
||||
|
||||
_, stopped = lc.Check(100 * time.Millisecond)
|
||||
assert.True(t, stopped)
|
||||
}
|
||||
|
||||
func TestErrorQuery(t *testing.T) {
|
||||
lc, cleanup := test.NewLogChecker("error-query")
|
||||
lc.StopMark("[Error SQL Query]")
|
||||
defer cleanup()
|
||||
|
||||
e := db.GetEngine(db.DefaultContext)
|
||||
engine, ok := e.(*xorm.Engine)
|
||||
assert.True(t, ok)
|
||||
|
||||
// It's not possible to clean this up with XORM, but it's luckily not harmful
|
||||
// to leave around.
|
||||
engine.AddHook(&db.ErrorQueryHook{
|
||||
Logger: log.GetLogger("error-query"),
|
||||
})
|
||||
|
||||
// Valid query.
|
||||
e.Exec("SELECT 1 WHERE false;")
|
||||
|
||||
_, stopped := lc.Check(100 * time.Millisecond)
|
||||
assert.False(t, stopped)
|
||||
|
||||
// Table doesn't exist.
|
||||
e.Exec("SELECT column FROM table;")
|
||||
|
||||
_, stopped = lc.Check(100 * time.Millisecond)
|
||||
assert.True(t, stopped)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
-
|
||||
id: 1000
|
||||
owner_id: 2
|
||||
name: user2@localhost
|
||||
fingerprint: "SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4"
|
||||
content: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKBknvWcuxM/W0iXGkzY4f2O6feX+Q7o46pKcxUbcOgh user2@localhost"
|
||||
# private key (base64-ed) LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNDZ1pKNzFuTHNUUDF0SWx4cE0yT0g5anVuM2wva082T09xU25NVkczRG9JUUFBQUpocG43YTZhWisyCnVnQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQ2daSjcxbkxzVFAxdElseHBNMk9IOWp1bjNsL2tPNk9PcVNuTVZHM0RvSVEKQUFBRUFxVm12bmo1LzZ5TW12ck9Ub29xa3F5MmUrc21aK0tBcEtKR0crRnY5MlA2QmtudldjdXhNL1cwaVhHa3pZNGYyTwo2ZmVYK1E3bzQ2cEtjeFViY09naEFBQUFFMmQxYzNSbFpFQm5kWE4wWldRdFltVmhjM1FCQWc9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0=
|
||||
mode: 2
|
||||
type: 1
|
||||
verified: true
|
||||
created_unix: 1559593109
|
||||
updated_unix: 1565224552
|
||||
login_source_id: 0
|
|
@ -150,3 +150,17 @@
|
|||
is_prerelease: false
|
||||
is_tag: false
|
||||
created_unix: 946684803
|
||||
|
||||
- id: 12
|
||||
repo_id: 1059
|
||||
publisher_id: 2
|
||||
tag_name: "v1.0"
|
||||
lower_tag_name: "v1.0"
|
||||
target: "main"
|
||||
title: "v1.0"
|
||||
sha1: "d8f53dfb33f6ccf4169c34970b5e747511c18beb"
|
||||
num_commits: 1
|
||||
is_draft: false
|
||||
is_prerelease: false
|
||||
is_tag: false
|
||||
created_unix: 946684803
|
||||
|
|
|
@ -608,6 +608,38 @@
|
|||
type: 1
|
||||
created_unix: 946684810
|
||||
|
||||
# BEGIN Forgejo [GITEA] Improve HTML title on repositories
|
||||
-
|
||||
id: 1093
|
||||
repo_id: 1059
|
||||
type: 1
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 1094
|
||||
repo_id: 1059
|
||||
type: 2
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 1095
|
||||
repo_id: 1059
|
||||
type: 3
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 1096
|
||||
repo_id: 1059
|
||||
type: 4
|
||||
created_unix: 946684810
|
||||
|
||||
-
|
||||
id: 1097
|
||||
repo_id: 1059
|
||||
type: 5
|
||||
created_unix: 946684810
|
||||
# END Forgejo [GITEA] Improve HTML title on repositories
|
||||
|
||||
-
|
||||
id: 91
|
||||
repo_id: 58
|
||||
|
|
|
@ -1467,6 +1467,7 @@
|
|||
owner_name: user27
|
||||
lower_name: repo49
|
||||
name: repo49
|
||||
description: A wonderful repository with more than just a README.md
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
|
@ -1694,6 +1695,19 @@
|
|||
is_fsck_enabled: true
|
||||
close_issues_via_commit_in_any_branch: false
|
||||
|
||||
-
|
||||
id: 1059
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: repo59
|
||||
name: repo59
|
||||
default_branch: master
|
||||
is_empty: false
|
||||
is_archived: false
|
||||
is_private: false
|
||||
status: 0
|
||||
num_issues: 0
|
||||
|
||||
-
|
||||
id: 59
|
||||
owner_id: 2
|
||||
|
|
|
@ -66,7 +66,7 @@
|
|||
num_followers: 2
|
||||
num_following: 1
|
||||
num_stars: 2
|
||||
num_repos: 15
|
||||
num_repos: 16
|
||||
num_teams: 0
|
||||
num_members: 0
|
||||
visibility: 0
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/models/forgejo/semver"
|
||||
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/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -43,6 +44,10 @@ var migrations = []*Migration{
|
|||
NewMigration("create the forgejo_sem_ver table", forgejo_v1_20.CreateSemVerTable),
|
||||
// v2 -> v3
|
||||
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.
|
||||
|
|
17
models/forgejo_migrations/v1_22/v4.go
Normal file
17
models/forgejo_migrations/v1_22/v4.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_22 //nolint
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddDefaultPermissionsToRepoUnit(x *xorm.Engine) error {
|
||||
type RepoUnit struct {
|
||||
ID int64
|
||||
DefaultPermissions int `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
return x.Sync(&RepoUnit{})
|
||||
}
|
22
models/forgejo_migrations/v1_22/v5.go
Normal file
22
models/forgejo_migrations/v1_22/v5.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_22 //nolint
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type RepoFlag struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
|
||||
Name string `xorm:"UNIQUE(s) INDEX"`
|
||||
}
|
||||
|
||||
func (RepoFlag) TableName() string {
|
||||
return "forgejo_repo_flag"
|
||||
}
|
||||
|
||||
func CreateRepoFlagTable(x *xorm.Engine) error {
|
||||
return x.Sync(new(RepoFlag))
|
||||
}
|
|
@ -128,6 +128,10 @@ func (b *Branch) LoadDeletedBy(ctx context.Context) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
func (b *Branch) GetRepo(ctx context.Context) (*repo_model.Repository, error) {
|
||||
return repo_model.GetRepositoryByID(ctx, b.RepoID)
|
||||
}
|
||||
|
||||
func (b *Branch) LoadPusher(ctx context.Context) (err error) {
|
||||
if b.Pusher == nil && b.PusherID > 0 {
|
||||
b.Pusher, err = user_model.GetUserByID(ctx, b.PusherID)
|
||||
|
@ -283,7 +287,7 @@ func FindRenamedBranch(ctx context.Context, repoID int64, from string) (branch *
|
|||
}
|
||||
|
||||
// RenameBranch rename a branch
|
||||
func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(ctx context.Context, isDefault bool) error) (err error) {
|
||||
func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to string, gitAction func(isDefault bool) error) (err error) {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -358,7 +362,7 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
|
|||
}
|
||||
|
||||
// 5. do git action
|
||||
if err = gitAction(ctx, isDefault); err != nil {
|
||||
if err = gitAction(isDefault); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
package git_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -133,7 +132,7 @@ func TestRenameBranch(t *testing.T) {
|
|||
}, git_model.WhitelistOptions{}))
|
||||
assert.NoError(t, committer.Commit())
|
||||
|
||||
assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(ctx context.Context, isDefault bool) error {
|
||||
assert.NoError(t, git_model.RenameBranch(db.DefaultContext, repo1, "master", "main", func(isDefault bool) error {
|
||||
_isDefault = isDefault
|
||||
return nil
|
||||
}))
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
@ -97,3 +98,29 @@ func TestMigrate_InsertIssueComments(t *testing.T) {
|
|||
|
||||
unittest.CheckConsistencyFor(t, &issues_model.Issue{})
|
||||
}
|
||||
|
||||
func TestUpdateCommentsMigrationsByType(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
|
||||
comment := unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1, IssueID: issue.ID})
|
||||
|
||||
// Set repository to migrated from Gitea.
|
||||
repo.OriginalServiceType = structs.GiteaService
|
||||
repo_model.UpdateRepositoryCols(db.DefaultContext, repo, "original_service_type")
|
||||
|
||||
// Set comment to have an original author.
|
||||
comment.OriginalAuthor = "Example User"
|
||||
comment.OriginalAuthorID = 1
|
||||
comment.PosterID = 0
|
||||
_, err := db.GetEngine(db.DefaultContext).ID(comment.ID).Cols("original_author", "original_author_id", "poster_id").Update(comment)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.NoError(t, issues_model.UpdateCommentsMigrationsByType(db.DefaultContext, structs.GiteaService, "1", 513))
|
||||
|
||||
comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 1, IssueID: issue.ID})
|
||||
assert.Empty(t, comment.OriginalAuthor)
|
||||
assert.Empty(t, comment.OriginalAuthorID)
|
||||
assert.EqualValues(t, 513, comment.PosterID)
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
package v1_14 //nolint
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
|
|
|
@ -4,13 +4,7 @@
|
|||
package v1_21 //nolint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
giturl "code.gitea.io/gitea/modules/git/url"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
|
@ -73,7 +67,7 @@ func migratePullMirrors(x *xorm.Engine) error {
|
|||
start += len(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 {
|
||||
return err
|
||||
}
|
||||
|
@ -136,7 +130,7 @@ func migratePushMirrors(x *xorm.Engine) error {
|
|||
start += len(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 {
|
||||
return err
|
||||
}
|
||||
|
@ -160,20 +154,3 @@ func migratePushMirrors(x *xorm.Engine) error {
|
|||
|
||||
return sess.Commit()
|
||||
}
|
||||
|
||||
func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
|
||||
repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git")
|
||||
|
||||
remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)
|
||||
}
|
||||
|
||||
u, err := giturl.Parse(remoteURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u.User = nil
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
|
|
@ -33,6 +33,16 @@ func (p *Permission) IsAdmin() bool {
|
|||
return p.AccessMode >= perm_model.AccessModeAdmin
|
||||
}
|
||||
|
||||
// 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
|
||||
func (p *Permission) HasAccess() bool {
|
||||
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 {
|
||||
return perm, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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 _, ok := perm.UnitsMode[u.Type]; !ok {
|
||||
perm.UnitsMode[u.Type] = perm_model.AccessModeRead
|
||||
perm.UnitsMode[u.Type] = u.DefaultPermissions.ToAccessMode(perm_model.AccessModeRead)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ func GetScheduledMergeByPullID(ctx context.Context, pullID int64) (bool, *AutoMe
|
|||
return false, nil, err
|
||||
}
|
||||
|
||||
doer, err := user_model.GetUserByID(ctx, scheduledPRM.DoerID)
|
||||
doer, err := user_model.GetPossibleUserByID(ctx, scheduledPRM.DoerID)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
|
|
@ -5,10 +5,16 @@ package repo
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"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)
|
||||
}
|
||||
|
||||
// GetPushMirrorRemoteAddress returns the address of associated with a repository's given remote.
|
||||
func GetPushMirrorRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
|
||||
repoPath := filepath.Join(setting.RepoRootPath, strings.ToLower(ownerName), strings.ToLower(repoName)+".git")
|
||||
|
||||
remoteURL, err := git.GetRemoteAddress(context.Background(), repoPath, remoteName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)
|
||||
}
|
||||
|
||||
u, err := giturl.Parse(remoteURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u.User = nil
|
||||
|
||||
return u.String(), nil
|
||||
}
|
||||
|
|
102
models/repo/repo_flags.go
Normal file
102
models/repo/repo_flags.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// RepoFlag represents a single flag against a repository
|
||||
type RepoFlag struct { //revive:disable-line:exported
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
RepoID int64 `xorm:"UNIQUE(s) INDEX"`
|
||||
Name string `xorm:"UNIQUE(s) INDEX"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
db.RegisterModel(new(RepoFlag))
|
||||
}
|
||||
|
||||
// TableName provides the real table name
|
||||
func (RepoFlag) TableName() string {
|
||||
return "forgejo_repo_flag"
|
||||
}
|
||||
|
||||
// ListFlags returns the array of flags on the repo.
|
||||
func (repo *Repository) ListFlags(ctx context.Context) ([]RepoFlag, error) {
|
||||
var flags []RepoFlag
|
||||
err := db.GetEngine(ctx).Table(&RepoFlag{}).Where("repo_id = ?", repo.ID).Find(&flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return flags, nil
|
||||
}
|
||||
|
||||
// IsFlagged returns whether a repo has any flags or not
|
||||
func (repo *Repository) IsFlagged(ctx context.Context) bool {
|
||||
has, _ := db.Exist[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID})
|
||||
return has
|
||||
}
|
||||
|
||||
// GetFlag returns a single RepoFlag based on its name
|
||||
func (repo *Repository) GetFlag(ctx context.Context, flagName string) (bool, *RepoFlag, error) {
|
||||
flag, has, err := db.Get[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID, "name": flagName})
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return has, flag, nil
|
||||
}
|
||||
|
||||
// HasFlag returns true if a repo has a given flag, false otherwise
|
||||
func (repo *Repository) HasFlag(ctx context.Context, flagName string) bool {
|
||||
has, _ := db.Exist[RepoFlag](ctx, builder.Eq{"repo_id": repo.ID, "name": flagName})
|
||||
return has
|
||||
}
|
||||
|
||||
// AddFlag adds a new flag to the repo
|
||||
func (repo *Repository) AddFlag(ctx context.Context, flagName string) error {
|
||||
return db.Insert(ctx, RepoFlag{
|
||||
RepoID: repo.ID,
|
||||
Name: flagName,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteFlag removes a flag from the repo
|
||||
func (repo *Repository) DeleteFlag(ctx context.Context, flagName string) (int64, error) {
|
||||
return db.DeleteByBean(ctx, &RepoFlag{RepoID: repo.ID, Name: flagName})
|
||||
}
|
||||
|
||||
// ReplaceAllFlags replaces all flags of a repo with a new set
|
||||
func (repo *Repository) ReplaceAllFlags(ctx context.Context, flagNames []string) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err := db.DeleteBeans(ctx, &RepoFlag{RepoID: repo.ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(flagNames) == 0 {
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
var flags []RepoFlag
|
||||
for _, name := range flagNames {
|
||||
flags = append(flags, RepoFlag{
|
||||
RepoID: repo.ID,
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
if err := db.Insert(ctx, &flags); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
114
models/repo/repo_flags_test.go
Normal file
114
models/repo/repo_flags_test.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRepositoryFlags(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||
|
||||
// ********************
|
||||
// ** NEGATIVE TESTS **
|
||||
// ********************
|
||||
|
||||
// Unless we add flags, the repo has none
|
||||
flags, err := repo.ListFlags(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, flags)
|
||||
|
||||
// If the repo has no flags, it is not flagged
|
||||
flagged := repo.IsFlagged(db.DefaultContext)
|
||||
assert.False(t, flagged)
|
||||
|
||||
// Trying to find a flag when there is none
|
||||
has := repo.HasFlag(db.DefaultContext, "foo")
|
||||
assert.False(t, has)
|
||||
|
||||
// Trying to retrieve a non-existent flag indicates not found
|
||||
has, _, err = repo.GetFlag(db.DefaultContext, "foo")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, has)
|
||||
|
||||
// Deleting a non-existent flag fails
|
||||
deleted, err := repo.DeleteFlag(db.DefaultContext, "no-such-flag")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(0), deleted)
|
||||
|
||||
// ********************
|
||||
// ** POSITIVE TESTS **
|
||||
// ********************
|
||||
|
||||
// Adding a flag works
|
||||
err = repo.AddFlag(db.DefaultContext, "foo")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Adding it again fails
|
||||
err = repo.AddFlag(db.DefaultContext, "foo")
|
||||
assert.Error(t, err)
|
||||
|
||||
// Listing flags includes the one we added
|
||||
flags, err = repo.ListFlags(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, flags, 1)
|
||||
assert.Equal(t, "foo", flags[0].Name)
|
||||
|
||||
// With a flag added, the repo is flagged
|
||||
flagged = repo.IsFlagged(db.DefaultContext)
|
||||
assert.True(t, flagged)
|
||||
|
||||
// The flag can be found
|
||||
has = repo.HasFlag(db.DefaultContext, "foo")
|
||||
assert.True(t, has)
|
||||
|
||||
// Added flag can be retrieved
|
||||
_, flag, err := repo.GetFlag(db.DefaultContext, "foo")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo", flag.Name)
|
||||
|
||||
// Deleting a flag works
|
||||
deleted, err = repo.DeleteFlag(db.DefaultContext, "foo")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, int64(1), deleted)
|
||||
|
||||
// The list is now empty
|
||||
flags, err = repo.ListFlags(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, flags)
|
||||
|
||||
// Replacing an empty list works
|
||||
err = repo.ReplaceAllFlags(db.DefaultContext, []string{"bar"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// The repo is now flagged with "bar"
|
||||
has = repo.HasFlag(db.DefaultContext, "bar")
|
||||
assert.True(t, has)
|
||||
|
||||
// Replacing a tag set with another works
|
||||
err = repo.ReplaceAllFlags(db.DefaultContext, []string{"baz", "quux"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// The repo now has two tags
|
||||
flags, err = repo.ListFlags(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, flags, 2)
|
||||
assert.Equal(t, "baz", flags[0].Name)
|
||||
assert.Equal(t, "quux", flags[1].Name)
|
||||
|
||||
// Replacing flags with an empty set deletes all flags
|
||||
err = repo.ReplaceAllFlags(db.DefaultContext, []string{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
// The repo is now unflagged
|
||||
flagged = repo.IsFlagged(db.DefaultContext)
|
||||
assert.False(t, flagged)
|
||||
}
|
|
@ -138,12 +138,12 @@ func getTestCases() []struct {
|
|||
{
|
||||
name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative",
|
||||
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",
|
||||
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",
|
||||
|
@ -158,7 +158,7 @@ func getTestCases() []struct {
|
|||
{
|
||||
name: "AllPublic/PublicRepositoriesOfOrganization",
|
||||
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",
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -39,13 +40,43 @@ func (err ErrUnitTypeNotExist) Unwrap() error {
|
|||
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
|
||||
type RepoUnit struct { //revive:disable-line:exported
|
||||
ID int64
|
||||
RepoID int64 `xorm:"INDEX(s)"`
|
||||
Type unit.Type `xorm:"INDEX(s)"`
|
||||
Config convert.Conversion `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
ID int64
|
||||
RepoID int64 `xorm:"INDEX(s)"`
|
||||
Type unit.Type `xorm:"INDEX(s)"`
|
||||
Config convert.Conversion `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED"`
|
||||
DefaultPermissions UnitAccessMode `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -283,3 +314,29 @@ func UpdateRepoUnit(ctx context.Context, unit *RepoUnit) error {
|
|||
_, err := db.GetEngine(ctx).ID(unit.ID).Update(unit)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateRepositoryUnits updates a repository's units
|
||||
func UpdateRepositoryUnits(ctx context.Context, repo *Repository, units []RepoUnit, deleteUnitTypes []unit.Type) (err error) {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
// Delete existing settings of units before adding again
|
||||
for _, u := range units {
|
||||
deleteUnitTypes = append(deleteUnitTypes, u.Type)
|
||||
}
|
||||
|
||||
if _, err = db.GetEngine(ctx).Where("repo_id = ?", repo.ID).In("type", deleteUnitTypes).Delete(new(RepoUnit)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(units) > 0 {
|
||||
if err = db.Insert(ctx, units); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ package repo
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
@ -28,3 +30,10 @@ func TestActionsConfig(t *testing.T) {
|
|||
cfg.DisableWorkflow("test3.yaml")
|
||||
assert.EqualValues(t, "test1.yaml,test2.yaml,test3.yaml", cfg.ToString())
|
||||
}
|
||||
|
||||
func TestRepoUnitAccessMode(t *testing.T) {
|
||||
assert.Equal(t, UnitAccessModeNone.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeNone)
|
||||
assert.Equal(t, UnitAccessModeRead.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeRead)
|
||||
assert.Equal(t, UnitAccessModeWrite.ToAccessMode(perm.AccessModeAdmin), perm.AccessModeWrite)
|
||||
assert.Equal(t, UnitAccessModeUnset.ToAccessMode(perm.AccessModeRead), perm.AccessModeRead)
|
||||
}
|
||||
|
|
|
@ -199,7 +199,7 @@ func FindTopics(ctx context.Context, opts *FindTopicOptions) ([]*Topic, int64, e
|
|||
sess.Join("INNER", "repo_topic", "repo_topic.topic_id = topic.id")
|
||||
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)
|
||||
}
|
||||
topics := make([]*Topic, 0, 10)
|
||||
|
|
113
models/unittest/mock_http.go
Normal file
113
models/unittest/mock_http.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
// Copyright 2017 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package unittest
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Mocks HTTP responses of a third-party service (such as GitHub, GitLab…)
|
||||
// This has two modes:
|
||||
// - live mode: the requests made to the mock HTTP server are transmitted to the live
|
||||
// service, and responses are saved as test data files
|
||||
// - test mode: the responses to requests to the mock HTTP server are read from the
|
||||
// test data files
|
||||
func NewMockWebServer(t *testing.T, liveServerBaseURL, testDataDir string, liveMode bool) *httptest.Server {
|
||||
mockServerBaseURL := ""
|
||||
ignoredHeaders := []string{"cf-ray", "server", "date", "report-to", "nel", "x-request-id"}
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := NormalizedFullPath(r.URL)
|
||||
log.Info("Mock HTTP Server: got request for path %s", r.URL.Path)
|
||||
// TODO check request method (support POST?)
|
||||
fixturePath := fmt.Sprintf("%s/%s", testDataDir, strings.NewReplacer("/", "_", "?", "!").Replace(path))
|
||||
if liveMode {
|
||||
liveURL := fmt.Sprintf("%s%s", liveServerBaseURL, path)
|
||||
|
||||
request, err := http.NewRequest(r.Method, liveURL, nil)
|
||||
assert.NoError(t, err, "constructing an HTTP request to %s failed", liveURL)
|
||||
for headerName, headerValues := range r.Header {
|
||||
// do not pass on the encoding: let the Transport of the HTTP client handle that for us
|
||||
if strings.ToLower(headerName) != "accept-encoding" {
|
||||
for _, headerValue := range headerValues {
|
||||
request.Header.Add(headerName, headerValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
assert.NoError(t, err, "HTTP request to %s failed: %s", liveURL)
|
||||
|
||||
fixture, err := os.Create(fixturePath)
|
||||
assert.NoError(t, err, "failed to open the fixture file %s for writing", fixturePath)
|
||||
defer fixture.Close()
|
||||
fixtureWriter := bufio.NewWriter(fixture)
|
||||
|
||||
for headerName, headerValues := range response.Header {
|
||||
for _, headerValue := range headerValues {
|
||||
if !slices.Contains(ignoredHeaders, strings.ToLower(headerName)) {
|
||||
_, err := fixtureWriter.WriteString(fmt.Sprintf("%s: %s\n", headerName, headerValue))
|
||||
assert.NoError(t, err, "writing the header of the HTTP response to the fixture file failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
_, err = fixtureWriter.WriteString("\n")
|
||||
assert.NoError(t, err, "writing the header of the HTTP response to the fixture file failed")
|
||||
fixtureWriter.Flush()
|
||||
|
||||
log.Info("Mock HTTP Server: writing response to %s", fixturePath)
|
||||
_, err = io.Copy(fixture, response.Body)
|
||||
assert.NoError(t, err, "writing the body of the HTTP response to %s failed", liveURL)
|
||||
|
||||
err = fixture.Sync()
|
||||
assert.NoError(t, err, "writing the body of the HTTP response to the fixture file failed")
|
||||
}
|
||||
|
||||
fixture, err := os.ReadFile(fixturePath)
|
||||
assert.NoError(t, err, "missing mock HTTP response: "+fixturePath)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// replace any mention of the live HTTP service by the mocked host
|
||||
stringFixture := strings.ReplaceAll(string(fixture), liveServerBaseURL, mockServerBaseURL)
|
||||
// parse back the fixture file into a series of HTTP headers followed by response body
|
||||
lines := strings.Split(stringFixture, "\n")
|
||||
for idx, line := range lines {
|
||||
colonIndex := strings.Index(line, ": ")
|
||||
if colonIndex != -1 {
|
||||
w.Header().Set(line[0:colonIndex], line[colonIndex+2:])
|
||||
} else {
|
||||
// we reached the end of the headers (empty line), so what follows is the body
|
||||
responseBody := strings.Join(lines[idx+1:], "\n")
|
||||
_, err := w.Write([]byte(responseBody))
|
||||
assert.NoError(t, err, "writing the body of the HTTP response failed")
|
||||
break
|
||||
}
|
||||
}
|
||||
}))
|
||||
mockServerBaseURL = server.URL
|
||||
return server
|
||||
}
|
||||
|
||||
func NormalizedFullPath(url *url.URL) string {
|
||||
// TODO normalize path (remove trailing slash?)
|
||||
// TODO normalize RawQuery (order query parameters?)
|
||||
if len(url.Query()) == 0 {
|
||||
return url.EscapedPath()
|
||||
}
|
||||
return fmt.Sprintf("%s?%s", url.EscapedPath(), url.RawQuery)
|
||||
}
|
|
@ -189,6 +189,25 @@ func GetEmailAddresses(ctx context.Context, uid int64) ([]*EmailAddress, error)
|
|||
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
|
||||
func GetEmailAddressByID(ctx context.Context, uid, id int64) (*EmailAddress, error) {
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 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,
|
||||
Name: "",
|
||||
KeyID: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func makeEmailPrimary(ctx context.Context, user *User, email *EmailAddress) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -410,6 +405,57 @@ func MakeEmailPrimary(ctx context.Context, email *EmailAddress) error {
|
|||
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
|
||||
func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
|
||||
minutes := setting.Service.ActiveCodeLives
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package user_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
@ -166,6 +167,28 @@ func TestMakeEmailPrimary(t *testing.T) {
|
|||
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) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
|
@ -309,3 +332,37 @@ func TestEmailAddressValidate(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetActivatedEmailAddresses(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
testCases := []struct {
|
||||
UID int64
|
||||
expected []*user_model.ActivatedEmailAddress
|
||||
}{
|
||||
{
|
||||
UID: 1,
|
||||
expected: []*user_model.ActivatedEmailAddress{{ID: 9, Email: "user1@example.com"}, {ID: 33, Email: "user1-2@example.com"}, {ID: 34, Email: "user1-3@example.com"}},
|
||||
},
|
||||
{
|
||||
UID: 2,
|
||||
expected: []*user_model.ActivatedEmailAddress{{ID: 3, Email: "user2@example.com"}},
|
||||
},
|
||||
{
|
||||
UID: 4,
|
||||
expected: []*user_model.ActivatedEmailAddress{{ID: 11, Email: "user4@example.com"}},
|
||||
},
|
||||
{
|
||||
UID: 11,
|
||||
expected: []*user_model.ActivatedEmailAddress{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(fmt.Sprintf("User %d", testCase.UID), func(t *testing.T) {
|
||||
emails, err := user_model.GetActivatedEmailAddresses(db.DefaultContext, testCase.UID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testCase.expected, emails)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -228,6 +228,12 @@ func GetAllUsers(ctx context.Context) ([]*User, error) {
|
|||
return users, db.GetEngine(ctx).OrderBy("id").Where("type = ?", UserTypeIndividual).Find(&users)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (u *User) IsLocal() bool {
|
||||
return u.LoginType <= auth.Plain
|
||||
|
|
|
@ -533,6 +533,16 @@ func TestIsUserVisibleToViewer(t *testing.T) {
|
|||
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) {
|
||||
oldSetting := setting.Service.AllowedUserVisibilityModesSlice
|
||||
defer func() {
|
||||
|
@ -552,6 +562,11 @@ func Test_ValidateUser(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 {
|
||||
Input string
|
||||
Expected string
|
||||
|
|
|
@ -35,6 +35,9 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
|
|||
case GithubEventGollum:
|
||||
return triggedEvent == webhook_module.HookEventWiki
|
||||
|
||||
case GithubEventSchedule:
|
||||
return triggedEvent == webhook_module.HookEventSchedule
|
||||
|
||||
case GithubEventIssues:
|
||||
switch triggedEvent {
|
||||
case webhook_module.HookEventIssues,
|
||||
|
@ -70,9 +73,6 @@ func canGithubEventMatch(eventName string, triggedEvent webhook_module.HookEvent
|
|||
return false
|
||||
}
|
||||
|
||||
case GithubEventSchedule:
|
||||
return triggedEvent == webhook_module.HookEventSchedule
|
||||
|
||||
default:
|
||||
return eventName == string(triggedEvent)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import (
|
|||
|
||||
type DetectedWorkflow struct {
|
||||
EntryName string
|
||||
TriggerEvent *jobparser.Event
|
||||
TriggerEvent string
|
||||
Content []byte
|
||||
}
|
||||
|
||||
|
@ -103,7 +103,6 @@ func DetectWorkflows(
|
|||
commit *git.Commit,
|
||||
triggedEvent webhook_module.HookEventType,
|
||||
payload api.Payloader,
|
||||
detectSchedule bool,
|
||||
) ([]*DetectedWorkflow, []*DetectedWorkflow, error) {
|
||||
entries, err := ListWorkflows(commit)
|
||||
if err != nil {
|
||||
|
@ -118,7 +117,6 @@ func DetectWorkflows(
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
// one workflow may have multiple events
|
||||
events, err := GetEventsFromContent(content)
|
||||
if err != nil {
|
||||
log.Warn("ignore invalid workflow %q: %v", entry.Name(), err)
|
||||
|
@ -127,18 +125,17 @@ func DetectWorkflows(
|
|||
for _, evt := range events {
|
||||
log.Trace("detect workflow %q for event %#v matching %q", entry.Name(), evt, triggedEvent)
|
||||
if evt.IsSchedule() {
|
||||
if detectSchedule {
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
TriggerEvent: evt,
|
||||
Content: content,
|
||||
}
|
||||
schedules = append(schedules, dwf)
|
||||
}
|
||||
} else if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
TriggerEvent: evt,
|
||||
TriggerEvent: evt.Name,
|
||||
Content: content,
|
||||
}
|
||||
schedules = append(schedules, dwf)
|
||||
}
|
||||
if detectMatched(gitRepo, commit, triggedEvent, payload, evt) {
|
||||
dwf := &DetectedWorkflow{
|
||||
EntryName: entry.Name(),
|
||||
TriggerEvent: evt.Name,
|
||||
Content: content,
|
||||
}
|
||||
workflows = append(workflows, dwf)
|
||||
|
@ -156,11 +153,11 @@ func detectMatched(gitRepo *git.Repository, commit *git.Commit, triggedEvent web
|
|||
|
||||
switch triggedEvent {
|
||||
case // events with no activity types
|
||||
webhook_module.HookEventSchedule,
|
||||
webhook_module.HookEventCreate,
|
||||
webhook_module.HookEventDelete,
|
||||
webhook_module.HookEventFork,
|
||||
webhook_module.HookEventWiki,
|
||||
webhook_module.HookEventSchedule:
|
||||
webhook_module.HookEventWiki:
|
||||
if len(evt.Acts()) != 0 {
|
||||
log.Warn("Ignore unsupported %s event arguments %v", triggedEvent, evt.Acts())
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
package hash
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
|
|
|
@ -4,10 +4,9 @@
|
|||
package avatar
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strconv"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
// HashAvatar will generate a unique string, which ensures that when there's a
|
||||
|
|
|
@ -7,11 +7,10 @@
|
|||
package identicon
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
const minImageSize = 16
|
||||
|
|
|
@ -5,6 +5,7 @@ package base
|
|||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
@ -22,7 +23,6 @@ import (
|
|||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
// EncodeSha1 string to sha1 hex value.
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"net/url"
|
||||
"strings"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
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
|
||||
|
||||
Repo *Repository
|
||||
Comment *issues_model.Comment
|
||||
Org *APIOrganization
|
||||
Package *Package
|
||||
}
|
||||
|
|
|
@ -515,6 +515,62 @@ func GetCommitFileStatus(ctx context.Context, repoPath, commitID string) (*Commi
|
|||
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.
|
||||
func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, error) {
|
||||
commitID, _, err := NewCommand(ctx, "rev-parse").AddDynamicArguments(shortID).RunStdString(&RunOpts{Dir: repoPath})
|
||||
|
|
|
@ -278,3 +278,30 @@ func TestGetCommitFileStatusMerges(t *testing.T) {
|
|||
assert.Equal(t, commitFileStatus.Removed, expected.Removed)
|
||||
assert.Equal(t, commitFileStatus.Modified, expected.Modified)
|
||||
}
|
||||
|
||||
func TestParseCommitRenames(t *testing.T) {
|
||||
testcases := []struct {
|
||||
output string
|
||||
renames [][2]string
|
||||
}{
|
||||
{
|
||||
output: "R090\x00renamed.txt\x00history.txt\x00",
|
||||
renames: [][2]string{{"renamed.txt", "history.txt"}},
|
||||
},
|
||||
{
|
||||
output: "R090\x00renamed.txt\x00history.txt\x00R000\x00corruptedstdouthere",
|
||||
renames: [][2]string{{"renamed.txt", "history.txt"}},
|
||||
},
|
||||
{
|
||||
output: "R100\x00renamed.txt\x00history.txt\x00R001\x00readme.md\x00README.md\x00",
|
||||
renames: [][2]string{{"renamed.txt", "history.txt"}, {"readme.md", "README.md"}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testcases {
|
||||
renames := [][2]string{}
|
||||
parseCommitRenames(&renames, strings.NewReader(testcase.output))
|
||||
|
||||
assert.Equal(t, testcase.renames, renames)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,11 @@
|
|||
package git
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
// Cache represents a caching interface
|
||||
|
|
|
@ -291,7 +291,7 @@ func (repo *Repository) CheckAttributeReader(commitID string) (*CheckAttributeRe
|
|||
}
|
||||
|
||||
checker := &CheckAttributeReader{
|
||||
Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
|
||||
Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language", "linguist-documentation", "linguist-detectable"},
|
||||
Repo: repo,
|
||||
IndexFile: indexFilename,
|
||||
WorkTree: worktree,
|
||||
|
|
|
@ -13,6 +13,18 @@ const (
|
|||
bigFileSize int64 = 1024 * 1024 // 1 MiB
|
||||
)
|
||||
|
||||
type LinguistBoolAttrib struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func (attrib *LinguistBoolAttrib) IsTrue() bool {
|
||||
return attrib.Value == "set" || attrib.Value == "true"
|
||||
}
|
||||
|
||||
func (attrib *LinguistBoolAttrib) IsFalse() bool {
|
||||
return attrib.Value == "unset" || attrib.Value == "false"
|
||||
}
|
||||
|
||||
// mergeLanguageStats mergers language names with different cases. The name with most upper case letters is used.
|
||||
func mergeLanguageStats(stats map[string]int64) map[string]int64 {
|
||||
names := map[string]struct {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build gogit
|
||||
|
@ -57,23 +58,25 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
|||
return nil
|
||||
}
|
||||
|
||||
notVendored := false
|
||||
notGenerated := false
|
||||
isVendored := LinguistBoolAttrib{}
|
||||
isGenerated := LinguistBoolAttrib{}
|
||||
isDocumentation := LinguistBoolAttrib{}
|
||||
isDetectable := LinguistBoolAttrib{}
|
||||
|
||||
if checker != nil {
|
||||
attrs, err := checker.CheckPath(f.Name)
|
||||
if err == nil {
|
||||
if vendored, has := attrs["linguist-vendored"]; has {
|
||||
if vendored == "set" || vendored == "true" {
|
||||
return nil
|
||||
}
|
||||
notVendored = vendored == "false"
|
||||
isVendored = LinguistBoolAttrib{Value: vendored}
|
||||
}
|
||||
if generated, has := attrs["linguist-generated"]; has {
|
||||
if generated == "set" || generated == "true" {
|
||||
return nil
|
||||
}
|
||||
notGenerated = generated == "false"
|
||||
isGenerated = LinguistBoolAttrib{Value: generated}
|
||||
}
|
||||
if documentation, has := attrs["linguist-documentation"]; has {
|
||||
isDocumentation = LinguistBoolAttrib{Value: documentation}
|
||||
}
|
||||
if detectable, has := attrs["linguist-detectable"]; has {
|
||||
isDetectable = LinguistBoolAttrib{Value: detectable}
|
||||
}
|
||||
if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
|
||||
// group languages, such as Pug -> HTML; SCSS -> CSS
|
||||
|
@ -105,8 +108,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
|||
}
|
||||
}
|
||||
|
||||
if (!notVendored && analyze.IsVendor(f.Name)) || enry.IsDotFile(f.Name) ||
|
||||
enry.IsDocumentation(f.Name) || enry.IsConfiguration(f.Name) {
|
||||
if isDetectable.IsFalse() || isVendored.IsTrue() || isDocumentation.IsTrue() ||
|
||||
(!isVendored.IsFalse() && analyze.IsVendor(f.Name)) ||
|
||||
enry.IsDotFile(f.Name) ||
|
||||
enry.IsConfiguration(f.Name) ||
|
||||
(!isDocumentation.IsFalse() && enry.IsDocumentation(f.Name)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -115,12 +121,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
|||
if f.Size <= bigFileSize {
|
||||
content, _ = readFile(f, fileSizeLimit)
|
||||
}
|
||||
if !notGenerated && enry.IsGenerated(f.Name, content) {
|
||||
if !isGenerated.IsTrue() && enry.IsGenerated(f.Name, content) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Use .gitattributes file for linguist overrides
|
||||
|
||||
language := analyze.GetCodeLanguage(f.Name, content)
|
||||
if language == enry.OtherLanguage || language == "" {
|
||||
return nil
|
||||
|
@ -136,6 +141,13 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
|||
if !checked {
|
||||
langtype := enry.GetLanguageType(language)
|
||||
included = langtype == enry.Programming || langtype == enry.Markup
|
||||
if !included {
|
||||
if isDetectable.IsTrue() {
|
||||
included = true
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
includedLanguage[language] = included
|
||||
}
|
||||
if included {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2020 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !gogit
|
||||
|
@ -90,23 +91,25 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
|||
continue
|
||||
}
|
||||
|
||||
notVendored := false
|
||||
notGenerated := false
|
||||
isVendored := LinguistBoolAttrib{}
|
||||
isGenerated := LinguistBoolAttrib{}
|
||||
isDocumentation := LinguistBoolAttrib{}
|
||||
isDetectable := LinguistBoolAttrib{}
|
||||
|
||||
if checker != nil {
|
||||
attrs, err := checker.CheckPath(f.Name())
|
||||
if err == nil {
|
||||
if vendored, has := attrs["linguist-vendored"]; has {
|
||||
if vendored == "set" || vendored == "true" {
|
||||
continue
|
||||
}
|
||||
notVendored = vendored == "false"
|
||||
isVendored = LinguistBoolAttrib{Value: vendored}
|
||||
}
|
||||
if generated, has := attrs["linguist-generated"]; has {
|
||||
if generated == "set" || generated == "true" {
|
||||
continue
|
||||
}
|
||||
notGenerated = generated == "false"
|
||||
isGenerated = LinguistBoolAttrib{Value: generated}
|
||||
}
|
||||
if documentation, has := attrs["linguist-documentation"]; has {
|
||||
isDocumentation = LinguistBoolAttrib{Value: documentation}
|
||||
}
|
||||
if detectable, has := attrs["linguist-detectable"]; has {
|
||||
isDetectable = LinguistBoolAttrib{Value: detectable}
|
||||
}
|
||||
if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
|
||||
// group languages, such as Pug -> HTML; SCSS -> CSS
|
||||
|
@ -139,8 +142,11 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
|||
}
|
||||
}
|
||||
|
||||
if (!notVendored && analyze.IsVendor(f.Name())) || enry.IsDotFile(f.Name()) ||
|
||||
enry.IsDocumentation(f.Name()) || enry.IsConfiguration(f.Name()) {
|
||||
if isDetectable.IsFalse() || isVendored.IsTrue() || isDocumentation.IsTrue() ||
|
||||
(!isVendored.IsFalse() && analyze.IsVendor(f.Name())) ||
|
||||
enry.IsDotFile(f.Name()) ||
|
||||
enry.IsConfiguration(f.Name()) ||
|
||||
(!isDocumentation.IsFalse() && enry.IsDocumentation(f.Name())) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -173,7 +179,7 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
|||
return nil, err
|
||||
}
|
||||
}
|
||||
if !notGenerated && enry.IsGenerated(f.Name(), content) {
|
||||
if !isGenerated.IsTrue() && enry.IsGenerated(f.Name(), content) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -194,6 +200,13 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
|||
if !checked {
|
||||
langType := enry.GetLanguageType(language)
|
||||
included = langType == enry.Programming || langType == enry.Markup
|
||||
if !included {
|
||||
if isDetectable.IsTrue() {
|
||||
included = true
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
includedLanguage[language] = included
|
||||
}
|
||||
if included {
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
// Copyright 2015 The Gogs 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
|
||||
|
||||
package git
|
||||
|
||||
import "strings"
|
||||
|
||||
// GetBlobByPath get the blob object according the path
|
||||
func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
|
||||
entry, err := t.GetTreeEntryByPath(relpath)
|
||||
|
@ -17,3 +20,21 @@ func (t *Tree) GetBlobByPath(relpath string) (*Blob, error) {
|
|||
|
||||
return nil, ErrNotExist{"", relpath}
|
||||
}
|
||||
|
||||
// GetBlobByFoldedPath returns the blob object at relpath, regardless of the
|
||||
// case of relpath. If there are multiple files with the same case-insensitive
|
||||
// name, the first one found will be returned.
|
||||
func (t *Tree) GetBlobByFoldedPath(relpath string) (*Blob, error) {
|
||||
entries, err := t.ListEntries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if strings.EqualFold(entry.Name(), relpath) {
|
||||
return t.GetBlobByPath(entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrNotExist{"", relpath}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package lfs
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"hash"
|
||||
|
@ -12,8 +13,6 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package lfs
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -12,8 +13,6 @@ import (
|
|||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -29,12 +29,17 @@ func CleanValue(value []byte) []byte {
|
|||
value = bytes.TrimSpace(value)
|
||||
rs := bytes.Runes(value)
|
||||
result := make([]rune, 0, len(rs))
|
||||
needsDash := false
|
||||
for _, r := range rs {
|
||||
if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' || r == '-' {
|
||||
switch {
|
||||
case unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_':
|
||||
if needsDash && len(result) > 0 {
|
||||
result = append(result, '-')
|
||||
}
|
||||
needsDash = false
|
||||
result = append(result, unicode.ToLower(r))
|
||||
}
|
||||
if unicode.IsSpace(r) {
|
||||
result = append(result, '-')
|
||||
default:
|
||||
needsDash = true
|
||||
}
|
||||
}
|
||||
return []byte(string(result))
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2023 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
package common
|
||||
|
||||
|
@ -15,44 +16,45 @@ func TestCleanValue(t *testing.T) {
|
|||
}{
|
||||
// Github behavior test cases
|
||||
{"", ""},
|
||||
{"test(0)", "test0"},
|
||||
{"test!1", "test1"},
|
||||
{"test:2", "test2"},
|
||||
{"test*3", "test3"},
|
||||
{"test!4", "test4"},
|
||||
{"test:5", "test5"},
|
||||
{"test*6", "test6"},
|
||||
{"test:6 a", "test6-a"},
|
||||
{"test:6 !b", "test6-b"},
|
||||
{"test:ad # df", "testad--df"},
|
||||
{"test:ad #23 df 2*/*", "testad-23-df-2"},
|
||||
{"test:ad 23 df 2*/*", "testad-23-df-2"},
|
||||
{"test:ad # 23 df 2*/*", "testad--23-df-2"},
|
||||
{"test.0.1", "test-0-1"},
|
||||
{"test(0)", "test-0"},
|
||||
{"test!1", "test-1"},
|
||||
{"test:2", "test-2"},
|
||||
{"test*3", "test-3"},
|
||||
{"test!4", "test-4"},
|
||||
{"test:5", "test-5"},
|
||||
{"test*6", "test-6"},
|
||||
{"test:6 a", "test-6-a"},
|
||||
{"test:6 !b", "test-6-b"},
|
||||
{"test:ad # df", "test-ad-df"},
|
||||
{"test:ad #23 df 2*/*", "test-ad-23-df-2"},
|
||||
{"test:ad 23 df 2*/*", "test-ad-23-df-2"},
|
||||
{"test:ad # 23 df 2*/*", "test-ad-23-df-2"},
|
||||
{"Anchors in Markdown", "anchors-in-markdown"},
|
||||
{"a_b_c", "a_b_c"},
|
||||
{"a-b-c", "a-b-c"},
|
||||
{"a-b-c----", "a-b-c----"},
|
||||
{"test:6a", "test6a"},
|
||||
{"test:a6", "testa6"},
|
||||
{"tes a a a a", "tes-a-a---a--a"},
|
||||
{" tes a a a a ", "tes-a-a---a--a"},
|
||||
{"a-b-c----", "a-b-c"},
|
||||
{"test:6a", "test-6a"},
|
||||
{"test:a6", "test-a6"},
|
||||
{"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"},
|
||||
{"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(0)", "tes0"},
|
||||
{"tes{0}", "tes0"},
|
||||
{"tes[0]", "tes0"},
|
||||
{"test【0】", "test0"},
|
||||
{"tes…@a", "tesa"},
|
||||
{"tes(0)", "tes-0"},
|
||||
{"tes{0}", "tes-0"},
|
||||
{"tes[0]", "tes-0"},
|
||||
{"test【0】", "test-0"},
|
||||
{"tes…@a", "tes-a"},
|
||||
{"tes¥& a", "tes-a"},
|
||||
{"tes= a", "tes-a"},
|
||||
{"tes|a", "tesa"},
|
||||
{"tes\\a", "tesa"},
|
||||
{"tes/a", "tesa"},
|
||||
{"tes|a", "tes-a"},
|
||||
{"tes\\a", "tes-a"},
|
||||
{"tes/a", "tes-a"},
|
||||
{"a啊啊b", "a啊啊b"},
|
||||
{"c🤔️🤔️d", "cd"},
|
||||
{"a⚡a", "aa"},
|
||||
{"e.~f", "ef"},
|
||||
{"c🤔️🤔️d", "c-d"},
|
||||
{"a⚡a", "a-a"},
|
||||
{"e.~f", "e-f"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
assert.Equal(t, []byte(test.expect), CleanValue([]byte(test.param)), test.param)
|
||||
|
|
|
@ -137,6 +137,8 @@ func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc pa
|
|||
var base string
|
||||
if ctx.IsWiki {
|
||||
base = ctx.Links.WikiLink()
|
||||
} else if ctx.Links.HasBranchInfo() {
|
||||
base = ctx.Links.SrcLink()
|
||||
} else {
|
||||
base = ctx.Links.Base
|
||||
}
|
||||
|
|
|
@ -524,6 +524,18 @@ func TestMathBlock(t *testing.T) {
|
|||
"$$a$$",
|
||||
`<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 {
|
||||
|
@ -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) {
|
||||
testcases := []struct {
|
||||
testcase string
|
||||
|
@ -765,7 +975,7 @@ space</p>
|
|||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="/user/repo/src/branch/main/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/user/repo/src/branch/main/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
|
@ -878,7 +1088,7 @@ space</p>
|
|||
Expected: `<p>space @mention-user<br/>
|
||||
/just/a/path.bin<br/>
|
||||
<a href="https://example.com/file.bin" rel="nofollow">https://example.com/file.bin</a><br/>
|
||||
<a href="/user/repo/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="/user/repo/src/branch/main/sub/folder/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
<a href="/user/repo/src/branch/main/sub/folder/file.bin" rel="nofollow">local link</a><br/>
|
||||
<a href="https://example.com" rel="nofollow">remote link</a><br/>
|
||||
|
|
|
@ -55,10 +55,7 @@ func (b *blockParser) Open(parent ast.Node, reader text.Reader, pc parser.Contex
|
|||
return node, parser.Close | parser.NoChildren
|
||||
}
|
||||
|
||||
reader.Advance(segment.Len() - 1)
|
||||
segment.Start += 2
|
||||
node.Lines().Append(segment)
|
||||
return node, parser.NoChildren
|
||||
return nil, parser.NoChildren
|
||||
}
|
||||
|
||||
// Continue parses the current line and returns a result of parsing.
|
||||
|
|
|
@ -7,13 +7,12 @@ import (
|
|||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
// AesEncrypt encrypts text and given key with AES.
|
||||
|
|
|
@ -5,8 +5,9 @@ package setting
|
|||
|
||||
// Admin settings
|
||||
var Admin struct {
|
||||
DisableRegularOrgCreation bool
|
||||
DefaultEmailNotification string
|
||||
DisableRegularOrgCreation bool
|
||||
DefaultEmailNotification string
|
||||
SendNotificationEmailOnNewUser bool
|
||||
}
|
||||
|
||||
func loadAdminFrom(rootCfg ConfigProvider) {
|
||||
|
|
24
modules/setting/badges.go
Normal file
24
modules/setting/badges.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// Badges settings
|
||||
var Badges = struct {
|
||||
Enabled bool `ini:"ENABLED"`
|
||||
GeneratorURLTemplate string `ini:"GENERATOR_URL_TEMPLATE"`
|
||||
GeneratorURLTemplateTemplate *template.Template `ini:"-"`
|
||||
}{
|
||||
Enabled: true,
|
||||
GeneratorURLTemplate: "https://img.shields.io/badge/{{.label}}-{{.text}}-{{.color}}",
|
||||
}
|
||||
|
||||
func loadBadgesFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "badges", &Badges)
|
||||
|
||||
Badges.GeneratorURLTemplateTemplate = template.Must(template.New("").Parse(Badges.GeneratorURLTemplate))
|
||||
}
|
|
@ -45,6 +45,7 @@ var (
|
|||
ConnMaxLifetime time.Duration
|
||||
IterateBufferSize int
|
||||
AutoMigration bool
|
||||
SlowQueryTreshold time.Duration
|
||||
}{
|
||||
Timeout: 500,
|
||||
IterateBufferSize: 50,
|
||||
|
@ -87,6 +88,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
|
|||
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
|
||||
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
|
||||
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
|
||||
Database.SlowQueryTreshold = sec.Key("SLOW_QUERY_TRESHOLD").MustDuration(5 * time.Second)
|
||||
}
|
||||
|
||||
// DBConnStr returns database connection string
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
@ -19,6 +20,8 @@ const (
|
|||
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
|
||||
const ItemsPerPage = 40
|
||||
|
||||
|
@ -43,6 +46,7 @@ var (
|
|||
DisabledRepoUnits []string
|
||||
DefaultRepoUnits []string
|
||||
DefaultForkRepoUnits []string
|
||||
DownloadOrCloneMethods []string
|
||||
PrefixArchiveFiles bool
|
||||
DisableMigrations bool
|
||||
DisableStars bool `ini:"DISABLE_STARS"`
|
||||
|
@ -109,6 +113,9 @@ var (
|
|||
Wiki []string
|
||||
DefaultTrustModel string
|
||||
} `ini:"repository.signing"`
|
||||
|
||||
SettableFlags []string
|
||||
EnableFlags bool
|
||||
}{
|
||||
DetectedCharsetsOrder: []string{
|
||||
"UTF-8",
|
||||
|
@ -151,7 +158,7 @@ var (
|
|||
DefaultPrivate: RepoCreatingLastUserVisibility,
|
||||
DefaultPushCreatePrivate: true,
|
||||
MaxCreationLimit: -1,
|
||||
PreferredLicenses: []string{"Apache License 2.0", "MIT License"},
|
||||
PreferredLicenses: []string{"Apache-2.0", "MIT"},
|
||||
DisableHTTPGit: false,
|
||||
AccessControlAllowOrigin: "",
|
||||
UseCompatSSHURI: false,
|
||||
|
@ -161,6 +168,7 @@ var (
|
|||
DisabledRepoUnits: []string{},
|
||||
DefaultRepoUnits: []string{},
|
||||
DefaultForkRepoUnits: []string{},
|
||||
DownloadOrCloneMethods: []string{"download-zip", "download-targz", "download-bundle", "vscode-clone"},
|
||||
PrefixArchiveFiles: true,
|
||||
DisableMigrations: false,
|
||||
DisableStars: false,
|
||||
|
@ -265,6 +273,8 @@ var (
|
|||
Wiki: []string{"never"},
|
||||
DefaultTrustModel: "collaborator",
|
||||
},
|
||||
|
||||
EnableFlags: false,
|
||||
}
|
||||
RepoRootPath string
|
||||
ScriptType = "bash"
|
||||
|
@ -361,4 +371,12 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
|
|||
if err := loadRepoArchiveFrom(rootCfg); err != nil {
|
||||
log.Fatal("loadRepoArchiveFrom: %v", err)
|
||||
}
|
||||
|
||||
for _, method := range Repository.DownloadOrCloneMethods {
|
||||
if !slices.Contains(RecognisedRepositoryDownloadOrCloneMethods, method) {
|
||||
log.Error("Unrecognised repository download or clone method: %s", method)
|
||||
}
|
||||
}
|
||||
|
||||
Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool()
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ var Service = struct {
|
|||
DefaultKeepEmailPrivate bool
|
||||
DefaultAllowCreateOrganization bool
|
||||
DefaultUserIsRestricted bool
|
||||
AllowDotsInUsernames bool
|
||||
EnableTimetracking bool
|
||||
DefaultEnableTimetracking bool
|
||||
DefaultEnableDependencies bool
|
||||
|
@ -180,6 +181,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
|||
Service.DefaultKeepEmailPrivate = sec.Key("DEFAULT_KEEP_EMAIL_PRIVATE").MustBool()
|
||||
Service.DefaultAllowCreateOrganization = sec.Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").MustBool(true)
|
||||
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)
|
||||
if Service.EnableTimetracking {
|
||||
Service.DefaultEnableTimetracking = sec.Key("DEFAULT_ENABLE_TIMETRACKING").MustBool(true)
|
||||
|
|
|
@ -147,6 +147,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
|||
loadUIFrom(cfg)
|
||||
loadAdminFrom(cfg)
|
||||
loadAPIFrom(cfg)
|
||||
loadBadgesFrom(cfg)
|
||||
loadMetricsFrom(cfg)
|
||||
loadCamoFrom(cfg)
|
||||
loadI18nFrom(cfg)
|
||||
|
|
|
@ -402,6 +402,16 @@ func (p *PullRequestPayload) JSONPayload() ([]byte, error) {
|
|||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
type HookScheduleAction string
|
||||
|
||||
const (
|
||||
HookScheduleCreated HookScheduleAction = "schedule"
|
||||
)
|
||||
|
||||
type SchedulePayload struct {
|
||||
Action HookScheduleAction `json:"action"`
|
||||
}
|
||||
|
||||
// ReviewPayload FIXME
|
||||
type ReviewPayload struct {
|
||||
Type string `json:"type"`
|
||||
|
|
|
@ -89,6 +89,9 @@ type CreatePullReviewComment struct {
|
|||
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
|
||||
type SubmitPullReviewOptions struct {
|
||||
Event ReviewStateType `json:"event"`
|
||||
|
|
9
modules/structs/repo_flags.go
Normal file
9
modules/structs/repo_flags.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package structs
|
||||
|
||||
// ReplaceFlagsOption options when replacing the flags of a repository
|
||||
type ReplaceFlagsOption struct {
|
||||
Flags []string `json:"flags"`
|
||||
}
|
|
@ -96,6 +96,9 @@ func NewFuncMap() template.FuncMap {
|
|||
"AppDomain": func() string { // documented in mail-templates.md
|
||||
return setting.Domain
|
||||
},
|
||||
"RepoFlagsEnabled": func() bool {
|
||||
return setting.Repository.EnableFlags
|
||||
},
|
||||
"AssetVersion": func() string {
|
||||
return setting.AssetVersion
|
||||
},
|
||||
|
|
|
@ -7,10 +7,9 @@ import (
|
|||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
// GenerateKeyPair generates a public and private keypair
|
||||
|
|
|
@ -7,12 +7,12 @@ import (
|
|||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
|
@ -117,13 +117,20 @@ func IsValidExternalTrackerURLFormat(uri string) bool {
|
|||
}
|
||||
|
||||
var (
|
||||
validUsernamePattern = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`)
|
||||
invalidUsernamePattern = regexp.MustCompile(`[-._]{2,}|[-._]$`) // No consecutive or trailing non-alphanumeric chars
|
||||
validUsernamePatternWithDots = regexp.MustCompile(`^[\da-zA-Z][-.\w]*$`)
|
||||
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
|
||||
func IsValidUsername(name string) bool {
|
||||
// It is difficult to find a single pattern that is both readable and effective,
|
||||
// but it's easier to use positive and negative checks.
|
||||
return validUsernamePattern.MatchString(name) && !invalidUsernamePattern.MatchString(name)
|
||||
if setting.Service.AllowDotsInUsernames {
|
||||
return validUsernamePatternWithDots.MatchString(name) && !invalidUsernamePattern.MatchString(name)
|
||||
}
|
||||
|
||||
return validUsernamePatternWithoutDots.MatchString(name) && !invalidUsernamePattern.MatchString(name)
|
||||
}
|
||||
|
|
|
@ -155,7 +155,8 @@ func Test_IsValidExternalTrackerURLFormat(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIsValidUsername(t *testing.T) {
|
||||
func TestIsValidUsernameAllowDots(t *testing.T) {
|
||||
setting.Service.AllowDotsInUsernames = true
|
||||
tests := []struct {
|
||||
arg string
|
||||
want bool
|
||||
|
@ -185,3 +186,31 @@ func TestIsValidUsername(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidUsernameBanDots(t *testing.T) {
|
||||
setting.Service.AllowDotsInUsernames = false
|
||||
defer func() {
|
||||
setting.Service.AllowDotsInUsernames = true
|
||||
}()
|
||||
|
||||
tests := []struct {
|
||||
arg string
|
||||
want bool
|
||||
}{
|
||||
{arg: "a", want: true},
|
||||
{arg: "abc", want: true},
|
||||
{arg: "0.b-c", want: false},
|
||||
{arg: "a.b-c_d", want: false},
|
||||
{arg: ".abc", want: false},
|
||||
{arg: "abc.", want: false},
|
||||
{arg: "a..bc", want: false},
|
||||
{arg: "a...bc", want: false},
|
||||
{arg: "a.-bc", want: false},
|
||||
{arg: "a._bc", want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.arg, func(t *testing.T) {
|
||||
assert.Equalf(t, tt.want, IsValidUsername(tt.arg), "IsValidUsername[AllowDotsInUsernames=false](%v)", tt.arg)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,6 +147,16 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
if hp, ok := handler.(func(next http.Handler) http.HandlerFunc); ok {
|
||||
return func(next http.Handler) http.Handler {
|
||||
h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
routing.UpdateFuncInfo(req.Context(), funcInfo)
|
||||
h.ServeHTTP(resp, req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
provider := func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) {
|
||||
// wrap the response writer to check whether the response has been written
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"reflect"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"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:
|
||||
data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
|
||||
case validation.ErrUsername:
|
||||
data["ErrorMsg"] = trName + l.Tr("form.username_error")
|
||||
if setting.Service.AllowDotsInUsernames {
|
||||
data["ErrorMsg"] = trName + l.Tr("form.username_error")
|
||||
} else {
|
||||
data["ErrorMsg"] = trName + l.Tr("form.username_error_no_dots")
|
||||
}
|
||||
case validation.ErrInvalidGroupTeamMap:
|
||||
data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
|
||||
default:
|
||||
|
|
|
@ -53,6 +53,7 @@ func CommonTemplateContextData() ContextData {
|
|||
"ShowMilestonesDashboardPage": setting.Service.ShowMilestonesDashboardPage,
|
||||
"ShowFooterVersion": setting.Other.ShowFooterVersion,
|
||||
"DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives,
|
||||
"DownloadOrCloneMethods": setting.Repository.DownloadOrCloneMethods,
|
||||
|
||||
"EnableSwagger": setting.API.EnableSwagger,
|
||||
"EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn,
|
||||
|
|
|
@ -295,6 +295,7 @@ default_allow_create_organization = Allow Creation of Organizations by Default
|
|||
default_allow_create_organization_popup = Allow new user accounts to create organizations by default.
|
||||
default_enable_timetracking = Enable Time Tracking 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_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
|
||||
|
@ -367,7 +368,7 @@ forgot_password_title= Forgot Password
|
|||
forgot_password = Forgot password?
|
||||
sign_up_now = Need an account? Register now.
|
||||
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
|
||||
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.
|
||||
|
@ -377,6 +378,9 @@ prohibit_login = Sign In Prohibited
|
|||
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.
|
||||
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
|
||||
email_not_associate = The email address is not associated with any account.
|
||||
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.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.title = %[1]s, welcome to %[2]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.`
|
||||
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_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`
|
||||
unknown_error = Unknown error:
|
||||
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.
|
||||
|
||||
[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>
|
||||
owner = Owner
|
||||
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.
|
||||
use_template = Use this template
|
||||
clone_in_vsc = Clone in VS Code
|
||||
clone_in_vscodium = Clone in VS Codium
|
||||
download_zip = Download ZIP
|
||||
download_tar = Download TAR.GZ
|
||||
download_bundle = Download BUNDLE
|
||||
|
@ -1261,6 +1279,7 @@ editor.new_branch_name_desc = New branch name…
|
|||
editor.cancel = Cancel
|
||||
editor.filename_cannot_be_empty = The filename cannot be empty.
|
||||
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_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.
|
||||
|
@ -1299,6 +1318,8 @@ commits.find = Search
|
|||
commits.search_all = All Branches
|
||||
commits.author = Author
|
||||
commits.message = Message
|
||||
commits.browse_further = Browse further
|
||||
commits.renamed_from = Renamed from %s
|
||||
commits.date = Date
|
||||
commits.older = Older
|
||||
commits.newer = Newer
|
||||
|
@ -1849,7 +1870,7 @@ pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull re
|
|||
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.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
|
||||
|
||||
|
@ -1912,6 +1933,7 @@ wiki.page_title = Page title
|
|||
wiki.page_content = Page content
|
||||
wiki.default_commit_message = Write a note about this page update (optional).
|
||||
wiki.save_page = Save Page
|
||||
wiki.cancel = Cancel
|
||||
wiki.last_commit_info = %s edited this page %s
|
||||
wiki.edit_page_button = Edit
|
||||
wiki.new_page_button = New Page
|
||||
|
@ -2050,6 +2072,7 @@ settings.branches.update_default_branch = Update Default Branch
|
|||
settings.branches.add_new_rule = Add New Rule
|
||||
settings.advanced_settings = Advanced Settings
|
||||
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_external_wiki = Use External Wiki
|
||||
settings.external_wiki_url = External Wiki URL
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
|
@ -26,8 +27,6 @@ import (
|
|||
chef_module "code.gitea.io/gitea/modules/packages/chef"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -6,6 +6,7 @@ package maven
|
|||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
|
@ -26,8 +27,6 @@ import (
|
|||
maven_module "code.gitea.io/gitea/modules/packages/maven"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -545,5 +545,5 @@ func RenameUser(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
log.Trace("User name changed: %s -> %s", oldName, newName)
|
||||
ctx.Status(http.StatusOK)
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
// This documentation describes the Gitea API.
|
||||
//
|
||||
// Schemes: http, https
|
||||
// Schemes: https, http
|
||||
// BasePath: /api/v1
|
||||
// Version: {{AppVer | JSEscape | Safe}}
|
||||
// License: MIT http://opensource.org/licenses/MIT
|
||||
|
@ -73,6 +73,7 @@ import (
|
|||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
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) {
|
||||
return func(ctx *context.APIContext) {
|
||||
if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() {
|
||||
|
@ -1104,6 +1138,18 @@ func Routes() *web.Route {
|
|||
m.Get("/permission", repo.GetRepoPermissions)
|
||||
})
|
||||
}, 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("/reviewers", reqToken(), reqAnyRepoReader(), repo.GetReviewers)
|
||||
m.Group("/teams", func() {
|
||||
|
@ -1223,8 +1269,16 @@ func Routes() *web.Route {
|
|||
Get(repo.GetPullReview).
|
||||
Delete(reqToken(), repo.DeletePullReview).
|
||||
Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview)
|
||||
m.Combo("/comments").
|
||||
Get(repo.GetPullReviewComments)
|
||||
m.Group("/comments", func() {
|
||||
m.Combo("").
|
||||
Get(repo.GetPullReviewComments).
|
||||
Post(reqToken(), bind(api.CreatePullReviewCommentOptions{}), repo.CreatePullReviewComment)
|
||||
m.Group("/{comment}", func() {
|
||||
m.Combo("").
|
||||
Get(repo.GetPullReviewComment).
|
||||
Delete(reqToken(), repo.DeletePullReviewComment)
|
||||
}, commentAssignment("comment"))
|
||||
})
|
||||
m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview)
|
||||
m.Post("/undismissals", reqToken(), repo.UnDismissPullReview)
|
||||
})
|
||||
|
@ -1328,7 +1382,7 @@ func Routes() *web.Route {
|
|||
Patch(reqToken(), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueCommentAttachment).
|
||||
Delete(reqToken(), mustNotBeArchived, repo.DeleteIssueCommentAttachment)
|
||||
}, mustEnableAttachments)
|
||||
})
|
||||
}, commentAssignment(":id"))
|
||||
})
|
||||
m.Group("/{index}", func() {
|
||||
m.Combo("").Get(repo.GetIssue).
|
||||
|
|
|
@ -261,7 +261,9 @@ func GetArchive(ctx *context.APIContext) {
|
|||
// ---
|
||||
// summary: Get an archive of a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// - application/octet-stream
|
||||
// - application/zip
|
||||
// - application/gzip
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
|
@ -342,7 +344,17 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model.
|
|||
}
|
||||
defer fr.Close()
|
||||
|
||||
contentType := ""
|
||||
switch archiver.Type {
|
||||
case git.ZIP:
|
||||
contentType = "application/zip"
|
||||
case git.TARGZ:
|
||||
// Per RFC6713.
|
||||
contentType = "application/gzip"
|
||||
}
|
||||
|
||||
ctx.ServeContent(fr, &context.ServeHeaderOptions{
|
||||
ContentType: contentType,
|
||||
Filename: downloadName,
|
||||
LastModified: archiver.CreatedUnix.AsLocalTime(),
|
||||
})
|
||||
|
|
245
routers/api/v1/repo/flags.go
Normal file
245
routers/api/v1/repo/flags.go
Normal file
|
@ -0,0 +1,245 @@
|
|||
// Copyright 2024 The Forgejo Authors c/o Codeberg e.V.. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
)
|
||||
|
||||
func ListFlags(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/flags repository repoListFlags
|
||||
// ---
|
||||
// summary: List a repository's flags
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/StringSlice"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repoFlags, err := ctx.Repo.Repository.ListFlags(ctx)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
flags := make([]string, len(repoFlags))
|
||||
for i := range repoFlags {
|
||||
flags[i] = repoFlags[i].Name
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(int64(len(repoFlags)))
|
||||
ctx.JSON(http.StatusOK, flags)
|
||||
}
|
||||
|
||||
func ReplaceAllFlags(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /repos/{owner}/{repo}/flags repository repoReplaceAllFlags
|
||||
// ---
|
||||
// summary: Replace all flags of a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/ReplaceFlagsOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
flagsForm := web.GetForm(ctx).(*api.ReplaceFlagsOption)
|
||||
|
||||
if err := ctx.Repo.Repository.ReplaceAllFlags(ctx, flagsForm.Flags); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func DeleteAllFlags(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/flags repository repoDeleteAllFlags
|
||||
// ---
|
||||
// summary: Remove all flags from a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if err := ctx.Repo.Repository.ReplaceAllFlags(ctx, nil); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func HasFlag(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/flags/{flag} repository repoCheckFlag
|
||||
// ---
|
||||
// summary: Check if a repository has a given flag
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: flag
|
||||
// in: path
|
||||
// description: name of the flag
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
hasFlag := ctx.Repo.Repository.HasFlag(ctx, ctx.Params(":flag"))
|
||||
if hasFlag {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
} else {
|
||||
ctx.NotFound()
|
||||
}
|
||||
}
|
||||
|
||||
func AddFlag(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /repos/{owner}/{repo}/flags/{flag} repository repoAddFlag
|
||||
// ---
|
||||
// summary: Add a flag to a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: flag
|
||||
// in: path
|
||||
// description: name of the flag
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
flag := ctx.Params(":flag")
|
||||
|
||||
if ctx.Repo.Repository.HasFlag(ctx, flag) {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctx.Repo.Repository.AddFlag(ctx, flag); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func DeleteFlag(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/flags/{flag} repository repoDeleteFlag
|
||||
// ---
|
||||
// summary: Remove a flag from a repository
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: flag
|
||||
// in: path
|
||||
// description: name of the flag
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
flag := ctx.Params(":flag")
|
||||
|
||||
if _, err := ctx.Repo.Repository.DeleteFlag(ctx, flag); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
|
@ -454,29 +454,7 @@ func GetIssueComment(ctx *context.APIContext) {
|
|||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
|
||||
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
|
||||
}
|
||||
comment := ctx.Comment
|
||||
|
||||
if comment.Type != issues_model.CommentTypeComment {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
|
@ -587,25 +565,7 @@ func EditIssueCommentDeprecated(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption) {
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
|
||||
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
|
||||
}
|
||||
comment := ctx.Comment
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
|
@ -617,7 +577,7 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
|||
return
|
||||
}
|
||||
|
||||
err = comment.LoadIssue(ctx)
|
||||
err := comment.LoadIssue(ctx)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||
return
|
||||
|
@ -668,7 +628,7 @@ func DeleteIssueComment(ctx *context.APIContext) {
|
|||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
deleteIssueComment(ctx)
|
||||
deleteIssueComment(ctx, issues_model.CommentTypeComment)
|
||||
}
|
||||
|
||||
// DeleteIssueCommentDeprecated delete a comment from an issue
|
||||
|
@ -707,39 +667,21 @@ func DeleteIssueCommentDeprecated(ctx *context.APIContext) {
|
|||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
deleteIssueComment(ctx)
|
||||
deleteIssueComment(ctx, issues_model.CommentTypeComment)
|
||||
}
|
||||
|
||||
func deleteIssueComment(ctx *context.APIContext) {
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
|
||||
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
|
||||
}
|
||||
func deleteIssueComment(ctx *context.APIContext, commentType issues_model.CommentType) {
|
||||
comment := ctx.Comment
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
} else if comment.Type != issues_model.CommentTypeComment {
|
||||
} else if comment.Type != commentType {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -55,11 +55,8 @@ func GetIssueCommentAttachment(ctx *context.APIContext) {
|
|||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
|
||||
comment := getIssueCommentSafe(ctx)
|
||||
if comment == nil {
|
||||
return
|
||||
}
|
||||
attachment := getIssueCommentAttachmentSafeRead(ctx, comment)
|
||||
comment := ctx.Comment
|
||||
attachment := getIssueCommentAttachmentSafeRead(ctx)
|
||||
if attachment == nil {
|
||||
return
|
||||
}
|
||||
|
@ -101,10 +98,7 @@ func ListIssueCommentAttachments(ctx *context.APIContext) {
|
|||
// "$ref": "#/responses/AttachmentList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
comment := getIssueCommentSafe(ctx)
|
||||
if comment == nil {
|
||||
return
|
||||
}
|
||||
comment := ctx.Comment
|
||||
|
||||
if err := comment.LoadAttachments(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
|
||||
|
@ -166,14 +160,12 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
|||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
// Check if comment exists and load comment
|
||||
comment := getIssueCommentSafe(ctx)
|
||||
if comment == nil {
|
||||
|
||||
if !canUserWriteIssueCommentAttachment(ctx) {
|
||||
return
|
||||
}
|
||||
|
||||
if !canUserWriteIssueCommentAttachment(ctx, comment) {
|
||||
return
|
||||
}
|
||||
comment := ctx.Comment
|
||||
|
||||
updatedAt := ctx.Req.FormValue("updated_at")
|
||||
if len(updatedAt) != 0 {
|
||||
|
@ -341,42 +333,17 @@ func DeleteIssueCommentAttachment(ctx *context.APIContext) {
|
|||
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 {
|
||||
comment := getIssueCommentSafe(ctx)
|
||||
if comment == nil {
|
||||
if !canUserWriteIssueCommentAttachment(ctx) {
|
||||
return nil
|
||||
}
|
||||
if !canUserWriteIssueCommentAttachment(ctx, comment) {
|
||||
return nil
|
||||
}
|
||||
return getIssueCommentAttachmentSafeRead(ctx, comment)
|
||||
return getIssueCommentAttachmentSafeRead(ctx)
|
||||
}
|
||||
|
||||
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)
|
||||
if !canEditComment {
|
||||
ctx.Error(http.StatusForbidden, "", "user should have permission to edit comment")
|
||||
|
@ -386,7 +353,10 @@ func canUserWriteIssueCommentAttachment(ctx *context.APIContext, comment *issues
|
|||
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"))
|
||||
if err != nil {
|
||||
ctx.NotFoundOrServerError("GetAttachmentByID", repo_model.IsErrAttachmentNotExist, err)
|
||||
|
|
|
@ -51,30 +51,7 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
|
|||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
|
||||
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
|
||||
}
|
||||
comment := ctx.Comment
|
||||
|
||||
reactions, _, err := issues_model.FindCommentReactions(ctx, comment.IssueID, comment.ID)
|
||||
if err != nil {
|
||||
|
@ -188,30 +165,7 @@ func DeleteIssueCommentReaction(ctx *context.APIContext) {
|
|||
}
|
||||
|
||||
func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOption, isCreateType bool) {
|
||||
comment, err := issues_model.GetCommentByID(ctx, ctx.ParamsInt64(":id"))
|
||||
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
|
||||
}
|
||||
comment := ctx.Comment
|
||||
|
||||
if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
|
||||
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 {
|
||||
// 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 {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteCommentReaction", err)
|
||||
return
|
||||
|
|
|
@ -208,6 +208,152 @@ func GetPullReviewComments(ctx *context.APIContext) {
|
|||
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.CreateCodeCommentKnownReviewID(ctx,
|
||||
ctx.Doer,
|
||||
pr.Issue.Repo,
|
||||
pr.Issue,
|
||||
opts.Body,
|
||||
opts.Path,
|
||||
line,
|
||||
review.ID,
|
||||
)
|
||||
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
|
||||
func DeletePullReview(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id} repository repoDeletePullReview
|
||||
|
@ -869,6 +1015,53 @@ func UnDismissPullReview(ctx *context.APIContext) {
|
|||
dismissReview(ctx, "", false, false)
|
||||
}
|
||||
|
||||
// DeletePullReviewComment delete a pull review comment
|
||||
func DeletePullReviewComment(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments/{comment} repository repoDeletePullReviewComment
|
||||
// ---
|
||||
// summary: Delete 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:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
deleteIssueComment(ctx, issues_model.CommentTypeCode)
|
||||
}
|
||||
|
||||
func dismissReview(ctx *context.APIContext, msg string, isDismiss, dismissPriors bool) {
|
||||
if !ctx.Repo.IsAdmin() {
|
||||
ctx.Error(http.StatusForbidden, "", "Must be repo admin")
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue