mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-03 13:04:08 +02:00
Merge branch 'main' into lunny/issue_dev
This commit is contained in:
commit
479364a908
783 files changed
+17262
-11293
No files matched your search
@@ -88,7 +88,7 @@ func (run *ActionRun) RefLink() string {
|
||||
if refName.IsPull() {
|
||||
return run.Repo.Link() + "/pulls/" + refName.ShortName()
|
||||
}
|
||||
return git.RefURL(run.Repo.Link(), run.Ref)
|
||||
return run.Repo.Link() + "/src/" + refName.RefWebLinkPath()
|
||||
}
|
||||
|
||||
// PrettyRef return #id for pull ref or ShortName for others
|
||||
@@ -154,7 +154,7 @@ func (run *ActionRun) GetPushEventPayload() (*api.PushPayload, error) {
|
||||
}
|
||||
|
||||
func (run *ActionRun) GetPullRequestEventPayload() (*api.PullRequestPayload, error) {
|
||||
if run.Event == webhook_module.HookEventPullRequest || run.Event == webhook_module.HookEventPullRequestSync {
|
||||
if run.Event.IsPullRequest() {
|
||||
var payload api.PullRequestPayload
|
||||
if err := json.Unmarshal([]byte(run.EventPayload), &payload); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -72,9 +72,9 @@ func (at ActionType) String() string {
|
||||
case ActionRenameRepo:
|
||||
return "rename_repo"
|
||||
case ActionStarRepo:
|
||||
return "star_repo"
|
||||
return "star_repo" // will not displayed in feeds.tmpl
|
||||
case ActionWatchRepo:
|
||||
return "watch_repo"
|
||||
return "watch_repo" // will not displayed in feeds.tmpl
|
||||
case ActionCommitRepo:
|
||||
return "commit_repo"
|
||||
case ActionCreateIssue:
|
||||
@@ -355,7 +355,7 @@ func (a *Action) GetBranch() string {
|
||||
|
||||
// GetRefLink returns the action's ref link.
|
||||
func (a *Action) GetRefLink(ctx context.Context) string {
|
||||
return git.RefURL(a.GetRepoLink(ctx), a.RefName)
|
||||
return a.GetRepoLink(ctx) + "/src/" + git.RefName(a.RefName).RefWebLinkPath()
|
||||
}
|
||||
|
||||
// GetTag returns the action's repository tag.
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp"
|
||||
"github.com/keybase/go-crypto/openpgp/packet"
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
@@ -141,7 +141,11 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified
|
||||
// Parse Subkeys
|
||||
subkeys := make([]*GPGKey, len(e.Subkeys))
|
||||
for i, k := range e.Subkeys {
|
||||
subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, expiry)
|
||||
subkeyExpiry := expiry
|
||||
if k.Sig.KeyLifetimeSecs != nil {
|
||||
subkeyExpiry = k.PublicKey.CreationTime.Add(time.Duration(*k.Sig.KeyLifetimeSecs) * time.Second)
|
||||
}
|
||||
subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, subkeyExpiry)
|
||||
if err != nil {
|
||||
return nil, ErrGPGKeyParsing{ParseError: err}
|
||||
}
|
||||
@@ -156,7 +160,7 @@ func parseGPGKey(ctx context.Context, ownerID int64, e *openpgp.Entity, verified
|
||||
|
||||
emails := make([]*user_model.EmailAddress, 0, len(e.Identities))
|
||||
for _, ident := range e.Identities {
|
||||
if ident.Revocation != nil {
|
||||
if ident.Revoked(time.Now()) {
|
||||
continue
|
||||
}
|
||||
email := strings.ToLower(strings.TrimSpace(ident.UserId.Email))
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
)
|
||||
|
||||
// __________________ ________ ____ __.
|
||||
@@ -83,12 +83,12 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str
|
||||
verified := false
|
||||
// Handle provided signature
|
||||
if signature != "" {
|
||||
signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature))
|
||||
signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature), nil)
|
||||
if err != nil {
|
||||
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature))
|
||||
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature), nil)
|
||||
}
|
||||
if err != nil {
|
||||
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature))
|
||||
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil)
|
||||
}
|
||||
if err != nil {
|
||||
log.Error("Unable to validate token signature. Error: %v", err)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp/packet"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
)
|
||||
|
||||
// __________________ ________ ____ __.
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp"
|
||||
"github.com/keybase/go-crypto/openpgp/armor"
|
||||
"github.com/keybase/go-crypto/openpgp/packet"
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
)
|
||||
|
||||
// __________________ ________ ____ __.
|
||||
@@ -80,7 +80,7 @@ func base64DecPubKey(content string) (*packet.PublicKey, error) {
|
||||
return pkey, nil
|
||||
}
|
||||
|
||||
// getExpiryTime extract the expire time of primary key based on sig
|
||||
// getExpiryTime extract the expiry time of primary key based on sig
|
||||
func getExpiryTime(e *openpgp.Entity) time.Time {
|
||||
expiry := time.Time{}
|
||||
// Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165
|
||||
@@ -88,12 +88,12 @@ func getExpiryTime(e *openpgp.Entity) time.Time {
|
||||
for _, ident := range e.Identities {
|
||||
if selfSig == nil {
|
||||
selfSig = ident.SelfSignature
|
||||
} else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
|
||||
} else if ident.SelfSignature != nil && ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
|
||||
selfSig = ident.SelfSignature
|
||||
break
|
||||
}
|
||||
}
|
||||
if selfSig.KeyLifetimeSecs != nil {
|
||||
if selfSig != nil && selfSig.KeyLifetimeSecs != nil {
|
||||
expiry = e.PrimaryKey.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second)
|
||||
}
|
||||
return expiry
|
||||
|
||||
@@ -13,8 +13,10 @@ import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/keybase/go-crypto/openpgp/packet"
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCheckArmoredGPGKeyString(t *testing.T) {
|
||||
@@ -107,9 +109,8 @@ MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg==
|
||||
=i9b7
|
||||
-----END PGP PUBLIC KEY BLOCK-----`
|
||||
keys, err := checkArmoredGPGKeyString(testGPGArmor)
|
||||
if !assert.NotEmpty(t, keys) {
|
||||
return
|
||||
}
|
||||
require.NotEmpty(t, keys)
|
||||
|
||||
ekey := keys[0]
|
||||
assert.NoError(t, err, "Could not parse a valid GPG armored key", ekey)
|
||||
|
||||
@@ -403,3 +404,25 @@ func TestTryGetKeyIDFromSignature(t *testing.T) {
|
||||
IssuerFingerprint: []uint8{0xb, 0x23, 0x24, 0xc7, 0xe6, 0xfe, 0x4f, 0x3a, 0x6, 0x26, 0xc1, 0x21, 0x3, 0x8d, 0x1a, 0x3e, 0xad, 0xdb, 0xea, 0x9c},
|
||||
}))
|
||||
}
|
||||
|
||||
func TestParseGPGKey(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
assert.NoError(t, db.Insert(db.DefaultContext, &user_model.EmailAddress{UID: 1, Email: "email1@example.com", IsActivated: true}))
|
||||
|
||||
// create a key for test email
|
||||
e, err := openpgp.NewEntity("name", "comment", "email1@example.com", nil)
|
||||
require.NoError(t, err)
|
||||
k, err := parseGPGKey(db.DefaultContext, 1, e, true)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, k.KeyID)
|
||||
assert.NotEmpty(t, k.Emails) // the key is valid, matches the email
|
||||
|
||||
// then revoke the key
|
||||
for _, id := range e.Identities {
|
||||
id.Revocations = append(id.Revocations, &packet.Signature{RevocationReason: util.ToPointer(packet.KeyCompromised)})
|
||||
}
|
||||
k, err = parseGPGKey(db.DefaultContext, 1, e, true)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, k.KeyID)
|
||||
assert.Empty(t, k.Emails) // the key is revoked, matches no email
|
||||
}
|
||||
@@ -7,23 +7,36 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/gtprof"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"xorm.io/xorm/contexts"
|
||||
)
|
||||
|
||||
type SlowQueryHook struct {
|
||||
type EngineHook struct {
|
||||
Threshold time.Duration
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
var _ contexts.Hook = (*SlowQueryHook)(nil)
|
||||
var _ contexts.Hook = (*EngineHook)(nil)
|
||||
|
||||
func (*SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
return c.Ctx, nil
|
||||
func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase)
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
span := gtprof.GetContextSpan(c.Ctx)
|
||||
if span != nil {
|
||||
// Do not record SQL parameters here:
|
||||
// * It shouldn't expose the parameters because they contain sensitive information, end users need to report the trace details safely.
|
||||
// * Some parameters contain quite long texts, waste memory and are difficult to display.
|
||||
span.SetAttributeString(gtprof.TraceAttrDbSQL, c.SQL)
|
||||
span.End()
|
||||
} else {
|
||||
setting.PanicInDevOrTesting("span in database engine hook is nil")
|
||||
}
|
||||
if c.ExecuteTime >= h.Threshold {
|
||||
// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function
|
||||
// is being displayed (the function that ultimately wants to execute the query in the code)
|
||||
|
||||
@@ -72,7 +72,7 @@ func InitEngine(ctx context.Context) error {
|
||||
xe.SetDefaultContext(ctx)
|
||||
|
||||
if setting.Database.SlowQueryThreshold > 0 {
|
||||
xe.AddHook(&SlowQueryHook{
|
||||
xe.AddHook(&EngineHook{
|
||||
Threshold: setting.Database.SlowQueryThreshold,
|
||||
Logger: log.GetLogger("xorm"),
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
_ "code.gitea.io/gitea/cmd" // for TestPrimaryKeys
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDumpDatabase(t *testing.T) {
|
||||
@@ -62,9 +63,7 @@ func TestPrimaryKeys(t *testing.T) {
|
||||
// Import "code.gitea.io/gitea/cmd" to make sure each db.RegisterModel in init functions has been called.
|
||||
|
||||
beans, err := db.NamesToBean()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
whitelist := map[string]string{
|
||||
"the_table_name_to_skip_checking": "Write a note here to explain why",
|
||||
@@ -79,8 +78,6 @@ func TestPrimaryKeys(t *testing.T) {
|
||||
t.Logf("ignore %q because %q", table.Name, why)
|
||||
continue
|
||||
}
|
||||
if len(table.PrimaryKeys) == 0 {
|
||||
t.Errorf("table %q has no primary key", table.Name)
|
||||
}
|
||||
assert.NotEmpty(t, table.PrimaryKeys, "table %q has no primary key", table.Name)
|
||||
}
|
||||
}
|
||||
+6
-15
@@ -5,21 +5,12 @@ package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNameEmpty name is empty error
|
||||
ErrNameEmpty = util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument}
|
||||
|
||||
// AlphaDashDotPattern characters prohibited in a username (anything except A-Za-z0-9_.-)
|
||||
AlphaDashDotPattern = regexp.MustCompile(`[^\w-\.]`)
|
||||
)
|
||||
|
||||
// ErrNameReserved represents a "reserved name" error.
|
||||
type ErrNameReserved struct {
|
||||
Name string
|
||||
@@ -82,20 +73,20 @@ func (err ErrNameCharsNotAllowed) Unwrap() error {
|
||||
|
||||
// IsUsableName checks if name is reserved or pattern of name is not allowed
|
||||
// based on given reserved names and patterns.
|
||||
// Names are exact match, patterns can be prefix or suffix match with placeholder '*'.
|
||||
func IsUsableName(names, patterns []string, name string) error {
|
||||
// Names are exact match, patterns can be a prefix or suffix match with placeholder '*'.
|
||||
func IsUsableName(reservedNames, reservedPatterns []string, name string) error {
|
||||
name = strings.TrimSpace(strings.ToLower(name))
|
||||
if utf8.RuneCountInString(name) == 0 {
|
||||
return ErrNameEmpty
|
||||
return util.SilentWrap{Message: "name is empty", Err: util.ErrInvalidArgument}
|
||||
}
|
||||
|
||||
for i := range names {
|
||||
if name == names[i] {
|
||||
for i := range reservedNames {
|
||||
if name == reservedNames[i] {
|
||||
return ErrNameReserved{name}
|
||||
}
|
||||
}
|
||||
|
||||
for _, pat := range patterns {
|
||||
for _, pat := range reservedPatterns {
|
||||
if pat[0] == '*' && strings.HasSuffix(name, pat[1:]) ||
|
||||
(pat[len(pat)-1] == '*' && strings.HasPrefix(name, pat[:len(pat)-1])) {
|
||||
return ErrNamePatternNotAllowed{pat}
|
||||
|
||||
@@ -171,3 +171,9 @@
|
||||
user_id: 40
|
||||
repo_id: 61
|
||||
mode: 4
|
||||
|
||||
-
|
||||
id: 30
|
||||
user_id: 40
|
||||
repo_id: 1
|
||||
mode: 2
|
||||
@@ -1694,19 +1694,6 @@
|
||||
is_fsck_enabled: true
|
||||
close_issues_via_commit_in_any_branch: false
|
||||
|
||||
-
|
||||
id: 59
|
||||
owner_id: 2
|
||||
owner_name: user2
|
||||
lower_name: test_commit_revert
|
||||
name: test_commit_revert
|
||||
default_branch: main
|
||||
is_empty: false
|
||||
is_archived: false
|
||||
is_private: true
|
||||
status: 0
|
||||
num_issues: 0
|
||||
|
||||
-
|
||||
id: 60
|
||||
owner_id: 40
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
num_followers: 2
|
||||
num_following: 1
|
||||
num_stars: 2
|
||||
num_repos: 15
|
||||
num_repos: 14
|
||||
num_teams: 0
|
||||
num_members: 0
|
||||
visibility: 0
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}'
|
||||
is_active: true
|
||||
|
||||
-
|
||||
id: 4
|
||||
repo_id: 2
|
||||
@@ -29,3 +30,23 @@
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
|
||||
is_active: true
|
||||
|
||||
-
|
||||
id: 5
|
||||
repo_id: 0
|
||||
owner_id: 0
|
||||
url: www.example.com/url5
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
|
||||
is_active: true
|
||||
is_system_webhook: true
|
||||
|
||||
-
|
||||
id: 6
|
||||
repo_id: 0
|
||||
owner_id: 0
|
||||
url: www.example.com/url6
|
||||
content_type: 1 # json
|
||||
events: '{"push_only":true,"branch_filter":"{master,feature*}"}'
|
||||
is_active: true
|
||||
is_system_webhook: false
|
||||
@@ -167,6 +167,9 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e
|
||||
BranchName: branchName,
|
||||
}
|
||||
}
|
||||
// FIXME: this design is not right: it doesn't check `branch.IsDeleted`, it doesn't make sense to make callers to check IsDeleted again and again.
|
||||
// It causes inconsistency with `GetBranches` and `git.GetBranch`, and will lead to strange bugs
|
||||
// In the future, there should be 2 functions: `GetBranchExisting` and `GetBranchWithDeleted`
|
||||
return &branch, nil
|
||||
}
|
||||
|
||||
@@ -440,6 +443,8 @@ type FindRecentlyPushedNewBranchesOptions struct {
|
||||
}
|
||||
|
||||
type RecentlyPushedNewBranch struct {
|
||||
BranchRepo *repo_model.Repository
|
||||
BranchName string
|
||||
BranchDisplayName string
|
||||
BranchLink string
|
||||
BranchCompareURL string
|
||||
@@ -540,7 +545,9 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o
|
||||
branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName)
|
||||
}
|
||||
newBranches = append(newBranches, &RecentlyPushedNewBranch{
|
||||
BranchRepo: branch.Repo,
|
||||
BranchDisplayName: branchDisplayName,
|
||||
BranchName: branch.Name,
|
||||
BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)),
|
||||
BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name),
|
||||
CommitTime: branch.CommitTime,
|
||||
|
||||
@@ -112,8 +112,8 @@ const (
|
||||
CommentTypePRScheduledToAutoMerge // 34 pr was scheduled to auto merge when checks succeed
|
||||
CommentTypePRUnScheduledToAutoMerge // 35 pr was un scheduled to auto merge when checks succeed
|
||||
|
||||
CommentTypePin // 36 pin Issue
|
||||
CommentTypeUnpin // 37 unpin Issue
|
||||
CommentTypePin // 36 pin Issue/PullRequest
|
||||
CommentTypeUnpin // 37 unpin Issue/PullRequest
|
||||
|
||||
CommentTypeChangeTimeEstimate // 38 Change time estimate
|
||||
)
|
||||
|
||||
@@ -26,14 +26,14 @@ func (comments CommentList) LoadPosters(ctx context.Context) error {
|
||||
return c.PosterID, c.Poster == nil && c.PosterID > 0
|
||||
})
|
||||
|
||||
posterMaps, err := getPostersByIDs(ctx, posterIDs)
|
||||
posterMaps, err := user_model.GetUsersMapByIDs(ctx, posterIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
if comment.Poster == nil {
|
||||
comment.Poster = getPoster(comment.PosterID, posterMaps)
|
||||
comment.Poster = user_model.GetPossibleUserFromMap(comment.PosterID, posterMaps)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -41,7 +41,7 @@ func (comments CommentList) LoadPosters(ctx context.Context) error {
|
||||
|
||||
func (comments CommentList) getLabelIDs() []int64 {
|
||||
return container.FilterSlice(comments, func(comment *Comment) (int64, bool) {
|
||||
return comment.LabelID, comment.LabelID > 0
|
||||
return comment.LabelID, comment.LabelID > 0 && comment.Label == nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,6 +51,9 @@ func (comments CommentList) loadLabels(ctx context.Context) error {
|
||||
}
|
||||
|
||||
labelIDs := comments.getLabelIDs()
|
||||
if len(labelIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
commentLabels := make(map[int64]*Label, len(labelIDs))
|
||||
left := len(labelIDs)
|
||||
for left > 0 {
|
||||
@@ -118,8 +121,8 @@ func (comments CommentList) loadMilestones(ctx context.Context) error {
|
||||
milestoneIDs = milestoneIDs[limit:]
|
||||
}
|
||||
|
||||
for _, issue := range comments {
|
||||
issue.Milestone = milestoneMaps[issue.MilestoneID]
|
||||
for _, comment := range comments {
|
||||
comment.Milestone = milestoneMaps[comment.MilestoneID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -175,6 +178,9 @@ func (comments CommentList) loadAssignees(ctx context.Context) error {
|
||||
}
|
||||
|
||||
assigneeIDs := comments.getAssigneeIDs()
|
||||
if len(assigneeIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
assignees := make(map[int64]*user_model.User, len(assigneeIDs))
|
||||
left := len(assigneeIDs)
|
||||
for left > 0 {
|
||||
@@ -301,6 +307,9 @@ func (comments CommentList) loadDependentIssues(ctx context.Context) error {
|
||||
|
||||
e := db.GetEngine(ctx)
|
||||
issueIDs := comments.getDependentIssueIDs()
|
||||
if len(issueIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
issues := make(map[int64]*Issue, len(issueIDs))
|
||||
left := len(issueIDs)
|
||||
for left > 0 {
|
||||
@@ -427,6 +436,9 @@ func (comments CommentList) loadReviews(ctx context.Context) error {
|
||||
}
|
||||
|
||||
reviewIDs := comments.getReviewIDs()
|
||||
if len(reviewIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
reviews := make(map[int64]*Review, len(reviewIDs))
|
||||
if err := db.GetEngine(ctx).In("id", reviewIDs).Find(&reviews); err != nil {
|
||||
return err
|
||||
|
||||
+3
-33
@@ -46,23 +46,6 @@ func (err ErrIssueNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrIssueIsClosed represents a "IssueIsClosed" kind of error.
|
||||
type ErrIssueIsClosed struct {
|
||||
ID int64
|
||||
RepoID int64
|
||||
Index int64
|
||||
}
|
||||
|
||||
// IsErrIssueIsClosed checks if an error is a ErrIssueNotExist.
|
||||
func IsErrIssueIsClosed(err error) bool {
|
||||
_, ok := err.(ErrIssueIsClosed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrIssueIsClosed) Error() string {
|
||||
return fmt.Sprintf("issue is closed [id: %d, repo_id: %d, index: %d]", err.ID, err.RepoID, err.Index)
|
||||
}
|
||||
|
||||
// ErrNewIssueInsert is used when the INSERT statement in newIssue fails
|
||||
type ErrNewIssueInsert struct {
|
||||
OriginalError error
|
||||
@@ -78,22 +61,6 @@ func (err ErrNewIssueInsert) Error() string {
|
||||
return err.OriginalError.Error()
|
||||
}
|
||||
|
||||
// ErrIssueWasClosed is used when close a closed issue
|
||||
type ErrIssueWasClosed struct {
|
||||
ID int64
|
||||
Index int64
|
||||
}
|
||||
|
||||
// IsErrIssueWasClosed checks if an error is a ErrIssueWasClosed.
|
||||
func IsErrIssueWasClosed(err error) bool {
|
||||
_, ok := err.(ErrIssueWasClosed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrIssueWasClosed) Error() string {
|
||||
return fmt.Sprintf("Issue [%d] %d was already closed", err.ID, err.Index)
|
||||
}
|
||||
|
||||
var ErrIssueAlreadyChanged = util.NewInvalidArgumentErrorf("the issue is already changed")
|
||||
|
||||
// Issue represents an issue or pull request of repository.
|
||||
@@ -271,6 +238,9 @@ func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err
|
||||
IssueID: issue.ID,
|
||||
Type: tp,
|
||||
})
|
||||
for _, comment := range issue.Comments {
|
||||
comment.Issue = issue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -81,53 +81,19 @@ func (issues IssueList) LoadPosters(ctx context.Context) error {
|
||||
return issue.PosterID, issue.Poster == nil && issue.PosterID > 0
|
||||
})
|
||||
|
||||
posterMaps, err := getPostersByIDs(ctx, posterIDs)
|
||||
posterMaps, err := user_model.GetUsersMapByIDs(ctx, posterIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
if issue.Poster == nil {
|
||||
issue.Poster = getPoster(issue.PosterID, posterMaps)
|
||||
issue.Poster = user_model.GetPossibleUserFromMap(issue.PosterID, posterMaps)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPostersByIDs(ctx context.Context, posterIDs []int64) (map[int64]*user_model.User, error) {
|
||||
posterMaps := make(map[int64]*user_model.User, len(posterIDs))
|
||||
left := len(posterIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
err := db.GetEngine(ctx).
|
||||
In("id", posterIDs[:limit]).
|
||||
Find(&posterMaps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
left -= limit
|
||||
posterIDs = posterIDs[limit:]
|
||||
}
|
||||
return posterMaps, nil
|
||||
}
|
||||
|
||||
func getPoster(posterID int64, posterMaps map[int64]*user_model.User) *user_model.User {
|
||||
if posterID == user_model.ActionsUserID {
|
||||
return user_model.NewActionsUser()
|
||||
}
|
||||
if posterID <= 0 {
|
||||
return nil
|
||||
}
|
||||
poster, ok := posterMaps[posterID]
|
||||
if !ok {
|
||||
return user_model.NewGhostUser()
|
||||
}
|
||||
return poster
|
||||
}
|
||||
|
||||
func (issues IssueList) getIssueIDs() []int64 {
|
||||
ids := make([]int64, 0, len(issues))
|
||||
for _, issue := range issues {
|
||||
|
||||
@@ -38,13 +38,15 @@ func (issue *Issue) projectID(ctx context.Context) int64 {
|
||||
}
|
||||
|
||||
// ProjectColumnID return project column id if issue was assigned to one
|
||||
func (issue *Issue) ProjectColumnID(ctx context.Context) int64 {
|
||||
func (issue *Issue) ProjectColumnID(ctx context.Context) (int64, error) {
|
||||
var ip project_model.ProjectIssue
|
||||
has, err := db.GetEngine(ctx).Where("issue_id=?", issue.ID).Get(&ip)
|
||||
if err != nil || !has {
|
||||
return 0
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if !has {
|
||||
return 0, nil
|
||||
}
|
||||
return ip.ProjectColumnID
|
||||
return ip.ProjectColumnID, nil
|
||||
}
|
||||
|
||||
// LoadIssuesFromColumn load issues assigned to this column
|
||||
|
||||
@@ -107,7 +107,7 @@ func GetIssueStats(ctx context.Context, opts *IssuesOptions) (*IssueStats, error
|
||||
accum.YourRepositoriesCount += stats.YourRepositoriesCount
|
||||
accum.AssignCount += stats.AssignCount
|
||||
accum.CreateCount += stats.CreateCount
|
||||
accum.OpenCount += stats.MentionCount
|
||||
accum.MentionCount += stats.MentionCount
|
||||
accum.ReviewRequestedCount += stats.ReviewRequestedCount
|
||||
accum.ReviewedCount += stats.ReviewedCount
|
||||
i = chunk
|
||||
|
||||
@@ -28,38 +28,40 @@ import (
|
||||
|
||||
// UpdateIssueCols updates cols of issue
|
||||
func UpdateIssueCols(ctx context.Context, issue *Issue, cols ...string) error {
|
||||
if _, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
_, err := db.GetEngine(ctx).ID(issue.ID).Cols(cols...).Update(issue)
|
||||
return err
|
||||
}
|
||||
|
||||
func ChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isClosed, isMergePull bool) (*Comment, error) {
|
||||
// Reload the issue
|
||||
currentIssue, err := GetIssueByID(ctx, issue.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// ErrIssueIsClosed is used when close a closed issue
|
||||
type ErrIssueIsClosed struct {
|
||||
ID int64
|
||||
RepoID int64
|
||||
Index int64
|
||||
IsPull bool
|
||||
}
|
||||
|
||||
// Nothing should be performed if current status is same as target status
|
||||
if currentIssue.IsClosed == isClosed {
|
||||
if !issue.IsPull {
|
||||
return nil, ErrIssueWasClosed{
|
||||
ID: issue.ID,
|
||||
}
|
||||
}
|
||||
return nil, ErrPullWasClosed{
|
||||
ID: issue.ID,
|
||||
// IsErrIssueIsClosed checks if an error is a ErrIssueIsClosed.
|
||||
func IsErrIssueIsClosed(err error) bool {
|
||||
_, ok := err.(ErrIssueIsClosed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrIssueIsClosed) Error() string {
|
||||
return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already closed", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index)
|
||||
}
|
||||
|
||||
func SetIssueAsClosed(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) {
|
||||
if issue.IsClosed {
|
||||
return nil, ErrIssueIsClosed{
|
||||
ID: issue.ID,
|
||||
RepoID: issue.RepoID,
|
||||
Index: issue.Index,
|
||||
IsPull: issue.IsPull,
|
||||
}
|
||||
}
|
||||
|
||||
issue.IsClosed = isClosed
|
||||
return doChangeIssueStatus(ctx, issue, doer, isMergePull)
|
||||
}
|
||||
|
||||
func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.User, isMergePull bool) (*Comment, error) {
|
||||
// Check for open dependencies
|
||||
if issue.IsClosed && issue.Repo.IsDependenciesEnabled(ctx) {
|
||||
if issue.Repo.IsDependenciesEnabled(ctx) {
|
||||
// only check if dependencies are enabled and we're about to close an issue, otherwise reopening an issue would fail when there are unsatisfied dependencies
|
||||
noDeps, err := IssueNoDependenciesLeft(ctx, issue)
|
||||
if err != nil {
|
||||
@@ -71,16 +73,63 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use
|
||||
}
|
||||
}
|
||||
|
||||
if issue.IsClosed {
|
||||
issue.ClosedUnix = timeutil.TimeStampNow()
|
||||
} else {
|
||||
issue.ClosedUnix = 0
|
||||
}
|
||||
issue.IsClosed = true
|
||||
issue.ClosedUnix = timeutil.TimeStampNow()
|
||||
|
||||
if err := UpdateIssueCols(ctx, issue, "is_closed", "closed_unix"); err != nil {
|
||||
if cnt, err := db.GetEngine(ctx).ID(issue.ID).Cols("is_closed", "closed_unix").
|
||||
Where("is_closed = ?", false).
|
||||
Update(issue); err != nil {
|
||||
return nil, err
|
||||
} else if cnt != 1 {
|
||||
return nil, ErrIssueAlreadyChanged
|
||||
}
|
||||
|
||||
return updateIssueNumbers(ctx, issue, doer, util.Iif(isMergePull, CommentTypeMergePull, CommentTypeClose))
|
||||
}
|
||||
|
||||
// ErrIssueIsOpen is used when reopen an opened issue
|
||||
type ErrIssueIsOpen struct {
|
||||
ID int64
|
||||
RepoID int64
|
||||
IsPull bool
|
||||
Index int64
|
||||
}
|
||||
|
||||
// IsErrIssueIsOpen checks if an error is a ErrIssueIsOpen.
|
||||
func IsErrIssueIsOpen(err error) bool {
|
||||
_, ok := err.(ErrIssueIsOpen)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrIssueIsOpen) Error() string {
|
||||
return fmt.Sprintf("%s [id: %d, repo_id: %d, index: %d] is already open", util.Iif(err.IsPull, "Pull Request", "Issue"), err.ID, err.RepoID, err.Index)
|
||||
}
|
||||
|
||||
func setIssueAsReopen(ctx context.Context, issue *Issue, doer *user_model.User) (*Comment, error) {
|
||||
if !issue.IsClosed {
|
||||
return nil, ErrIssueIsOpen{
|
||||
ID: issue.ID,
|
||||
RepoID: issue.RepoID,
|
||||
Index: issue.Index,
|
||||
IsPull: issue.IsPull,
|
||||
}
|
||||
}
|
||||
|
||||
issue.IsClosed = false
|
||||
issue.ClosedUnix = 0
|
||||
|
||||
if cnt, err := db.GetEngine(ctx).ID(issue.ID).Cols("is_closed", "closed_unix").
|
||||
Where("is_closed = ?", true).
|
||||
Update(issue); err != nil {
|
||||
return nil, err
|
||||
} else if cnt != 1 {
|
||||
return nil, ErrIssueAlreadyChanged
|
||||
}
|
||||
|
||||
return updateIssueNumbers(ctx, issue, doer, CommentTypeReopen)
|
||||
}
|
||||
|
||||
func updateIssueNumbers(ctx context.Context, issue *Issue, doer *user_model.User, cmtType CommentType) (*Comment, error) {
|
||||
// Update issue count of labels
|
||||
if err := issue.LoadLabels(ctx); err != nil {
|
||||
return nil, err
|
||||
@@ -103,14 +152,6 @@ func doChangeIssueStatus(ctx context.Context, issue *Issue, doer *user_model.Use
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// New action comment
|
||||
cmtType := CommentTypeClose
|
||||
if !issue.IsClosed {
|
||||
cmtType = CommentTypeReopen
|
||||
} else if isMergePull {
|
||||
cmtType = CommentTypeMergePull
|
||||
}
|
||||
|
||||
return CreateComment(ctx, &CreateCommentOptions{
|
||||
Type: cmtType,
|
||||
Doer: doer,
|
||||
@@ -134,7 +175,7 @@ func CloseIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Comm
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
comment, err := ChangeIssueStatus(ctx, issue, doer, true, false)
|
||||
comment, err := SetIssueAsClosed(ctx, issue, doer, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -159,7 +200,7 @@ func ReopenIssue(ctx context.Context, issue *Issue, doer *user_model.User) (*Com
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
comment, err := ChangeIssueStatus(ctx, issue, doer, false, false)
|
||||
comment, err := setIssueAsReopen(ctx, issue, doer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+2
-18
@@ -80,22 +80,6 @@ func (err ErrPullRequestAlreadyExists) Unwrap() error {
|
||||
return util.ErrAlreadyExist
|
||||
}
|
||||
|
||||
// ErrPullWasClosed is used close a closed pull request
|
||||
type ErrPullWasClosed struct {
|
||||
ID int64
|
||||
Index int64
|
||||
}
|
||||
|
||||
// IsErrPullWasClosed checks if an error is a ErrErrPullWasClosed.
|
||||
func IsErrPullWasClosed(err error) bool {
|
||||
_, ok := err.(ErrPullWasClosed)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrPullWasClosed) Error() string {
|
||||
return fmt.Sprintf("Pull request [%d] %d was already closed", err.ID, err.Index)
|
||||
}
|
||||
|
||||
// PullRequestType defines pull request type
|
||||
type PullRequestType int
|
||||
|
||||
@@ -301,7 +285,7 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID)
|
||||
reviews, _, err := GetReviewsByIssueID(ctx, pr.Issue.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -320,7 +304,7 @@ func (pr *PullRequest) LoadRequestedReviewers(ctx context.Context) error {
|
||||
|
||||
// LoadRequestedReviewersTeams loads the requested reviewers teams.
|
||||
func (pr *PullRequest) LoadRequestedReviewersTeams(ctx context.Context) error {
|
||||
reviews, err := GetReviewsByIssueID(ctx, pr.Issue.ID)
|
||||
reviews, _, err := GetReviewsByIssueID(ctx, pr.Issue.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -166,6 +166,23 @@ func (prs PullRequestList) getRepositoryIDs() []int64 {
|
||||
return repoIDs.Values()
|
||||
}
|
||||
|
||||
func (prs PullRequestList) SetBaseRepo(baseRepo *repo_model.Repository) {
|
||||
for _, pr := range prs {
|
||||
if pr.BaseRepo == nil {
|
||||
pr.BaseRepo = baseRepo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (prs PullRequestList) SetHeadRepo(headRepo *repo_model.Repository) {
|
||||
for _, pr := range prs {
|
||||
if pr.HeadRepo == nil {
|
||||
pr.HeadRepo = headRepo
|
||||
pr.isHeadRepoLoaded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (prs PullRequestList) LoadRepositories(ctx context.Context) error {
|
||||
repoIDs := prs.getRepositoryIDs()
|
||||
reposMap := make(map[int64]*repo_model.Repository, len(repoIDs))
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
@@ -321,6 +322,11 @@ func valuesUser(m map[int64]*user_model.User) []*user_model.User {
|
||||
return values
|
||||
}
|
||||
|
||||
// newMigrationOriginalUser creates and returns a fake user for external user
|
||||
func newMigrationOriginalUser(name string) *user_model.User {
|
||||
return &user_model.User{ID: 0, Name: name, LowerName: strings.ToLower(name)}
|
||||
}
|
||||
|
||||
// LoadUsers loads reactions' all users
|
||||
func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Repository) ([]*user_model.User, error) {
|
||||
if len(list) == 0 {
|
||||
@@ -338,7 +344,7 @@ func (list ReactionList) LoadUsers(ctx context.Context, repo *repo_model.Reposit
|
||||
|
||||
for _, reaction := range list {
|
||||
if reaction.OriginalAuthor != "" {
|
||||
reaction.User = user_model.NewReplaceUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name()))
|
||||
reaction.User = newMigrationOriginalUser(fmt.Sprintf("%s(%s)", reaction.OriginalAuthor, repo.OriginalServiceType.Name()))
|
||||
} else if user, ok := userMaps[reaction.UserID]; ok {
|
||||
reaction.User = user
|
||||
} else {
|
||||
|
||||
@@ -930,17 +930,19 @@ func MarkConversation(ctx context.Context, comment *Comment, doer *user_model.Us
|
||||
}
|
||||
|
||||
// CanMarkConversation Add or remove Conversation mark for a code comment permission check
|
||||
// the PR writer , offfcial reviewer and poster can do it
|
||||
// the PR writer , official reviewer and poster can do it
|
||||
func CanMarkConversation(ctx context.Context, issue *Issue, doer *user_model.User) (permResult bool, err error) {
|
||||
if doer == nil || issue == nil {
|
||||
return false, fmt.Errorf("issue or doer is nil")
|
||||
}
|
||||
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if issue.Repo.IsArchived {
|
||||
return false, nil
|
||||
}
|
||||
if doer.ID != issue.PosterID {
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
p, err := access_model.GetUserRepoPermission(ctx, issue.Repo, doer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
||||
@@ -5,6 +5,8 @@ package issues
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
organization_model "code.gitea.io/gitea/models/organization"
|
||||
@@ -153,43 +155,60 @@ func CountReviews(ctx context.Context, opts FindReviewOptions) (int64, error) {
|
||||
return db.GetEngine(ctx).Where(opts.toCond()).Count(&Review{})
|
||||
}
|
||||
|
||||
// GetReviewersFromOriginalAuthorsByIssueID gets the latest review of each original authors for a pull request
|
||||
func GetReviewersFromOriginalAuthorsByIssueID(ctx context.Context, issueID int64) (ReviewList, error) {
|
||||
reviews := make([]*Review, 0, 10)
|
||||
|
||||
// Get latest review of each reviewer, sorted in order they were made
|
||||
if err := db.GetEngine(ctx).SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND original_author_id <> 0 GROUP BY issue_id, original_author_id) ORDER BY review.updated_unix ASC",
|
||||
issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest).
|
||||
Find(&reviews); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return reviews, nil
|
||||
}
|
||||
|
||||
// GetReviewsByIssueID gets the latest review of each reviewer for a pull request
|
||||
func GetReviewsByIssueID(ctx context.Context, issueID int64) (ReviewList, error) {
|
||||
// The first returned parameter is the latest review of each individual reviewer or team
|
||||
// The second returned parameter is the latest review of each original author which is migrated from other systems
|
||||
// The reviews are sorted by updated time
|
||||
func GetReviewsByIssueID(ctx context.Context, issueID int64) (latestReviews, migratedOriginalReviews ReviewList, err error) {
|
||||
reviews := make([]*Review, 0, 10)
|
||||
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
// Get latest review of each reviewer, sorted in order they were made
|
||||
if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id = 0 AND type in (?, ?, ?) AND dismissed = ? AND original_author_id = 0 GROUP BY issue_id, reviewer_id) ORDER BY review.updated_unix ASC",
|
||||
issueID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, false).
|
||||
Find(&reviews); err != nil {
|
||||
return nil, err
|
||||
// Get all reviews for the issue id
|
||||
if err := db.GetEngine(ctx).Where("issue_id=?", issueID).OrderBy("updated_unix ASC").Find(&reviews); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// filter them in memory to get the latest review of each reviewer
|
||||
// Since the reviews should not be too many for one issue, less than 100 commonly, it's acceptable to do this in memory
|
||||
// And since there are too less indexes in review table, it will be very slow to filter in the database
|
||||
reviewersMap := make(map[int64][]*Review) // key is reviewer id
|
||||
originalReviewersMap := make(map[int64][]*Review) // key is original author id
|
||||
reviewTeamsMap := make(map[int64][]*Review) // key is reviewer team id
|
||||
countedReivewTypes := []ReviewType{ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest}
|
||||
for _, review := range reviews {
|
||||
if review.ReviewerTeamID == 0 && slices.Contains(countedReivewTypes, review.Type) && !review.Dismissed {
|
||||
if review.OriginalAuthorID != 0 {
|
||||
originalReviewersMap[review.OriginalAuthorID] = append(originalReviewersMap[review.OriginalAuthorID], review)
|
||||
} else {
|
||||
reviewersMap[review.ReviewerID] = append(reviewersMap[review.ReviewerID], review)
|
||||
}
|
||||
} else if review.ReviewerTeamID != 0 && review.OriginalAuthorID == 0 {
|
||||
reviewTeamsMap[review.ReviewerTeamID] = append(reviewTeamsMap[review.ReviewerTeamID], review)
|
||||
}
|
||||
}
|
||||
|
||||
individualReviews := make([]*Review, 0, 10)
|
||||
for _, reviews := range reviewersMap {
|
||||
individualReviews = append(individualReviews, reviews[len(reviews)-1])
|
||||
}
|
||||
sort.Slice(individualReviews, func(i, j int) bool {
|
||||
return individualReviews[i].UpdatedUnix < individualReviews[j].UpdatedUnix
|
||||
})
|
||||
|
||||
originalReviews := make([]*Review, 0, 10)
|
||||
for _, reviews := range originalReviewersMap {
|
||||
originalReviews = append(originalReviews, reviews[len(reviews)-1])
|
||||
}
|
||||
sort.Slice(originalReviews, func(i, j int) bool {
|
||||
return originalReviews[i].UpdatedUnix < originalReviews[j].UpdatedUnix
|
||||
})
|
||||
|
||||
teamReviewRequests := make([]*Review, 0, 5)
|
||||
if err := sess.SQL("SELECT * FROM review WHERE id IN (SELECT max(id) as id FROM review WHERE issue_id = ? AND reviewer_team_id <> 0 AND original_author_id = 0 GROUP BY issue_id, reviewer_team_id) ORDER BY review.updated_unix ASC",
|
||||
issueID).
|
||||
Find(&teamReviewRequests); err != nil {
|
||||
return nil, err
|
||||
for _, reviews := range reviewTeamsMap {
|
||||
teamReviewRequests = append(teamReviewRequests, reviews[len(reviews)-1])
|
||||
}
|
||||
sort.Slice(teamReviewRequests, func(i, j int) bool {
|
||||
return teamReviewRequests[i].UpdatedUnix < teamReviewRequests[j].UpdatedUnix
|
||||
})
|
||||
|
||||
if len(teamReviewRequests) > 0 {
|
||||
reviews = append(reviews, teamReviewRequests...)
|
||||
}
|
||||
|
||||
return reviews, nil
|
||||
return append(individualReviews, teamReviewRequests...), originalReviews, nil
|
||||
}
|
||||
@@ -162,8 +162,9 @@ func TestGetReviewersByIssueID(t *testing.T) {
|
||||
},
|
||||
)
|
||||
|
||||
allReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID)
|
||||
allReviews, migratedReviews, err := issues_model.GetReviewsByIssueID(db.DefaultContext, issue.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, migratedReviews)
|
||||
for _, review := range allReviews {
|
||||
assert.NoError(t, review.LoadReviewer(db.DefaultContext))
|
||||
}
|
||||
|
||||
@@ -46,11 +46,6 @@ func (s Stopwatch) Seconds() int64 {
|
||||
return int64(timeutil.TimeStampNow() - s.CreatedUnix)
|
||||
}
|
||||
|
||||
// Duration returns a human-readable duration string based on local server time
|
||||
func (s Stopwatch) Duration() string {
|
||||
return util.SecToTime(s.Seconds())
|
||||
}
|
||||
|
||||
func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) {
|
||||
sw = new(Stopwatch)
|
||||
exists, err = db.GetEngine(ctx).
|
||||
@@ -201,7 +196,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss
|
||||
Doer: user,
|
||||
Issue: issue,
|
||||
Repo: issue.Repo,
|
||||
Content: util.SecToTime(timediff),
|
||||
Content: util.SecToHours(timediff),
|
||||
Type: CommentTypeStopTracking,
|
||||
TimeID: tt.ID,
|
||||
}); err != nil {
|
||||
|
||||
@@ -372,7 +372,8 @@ func prepareMigrationTasks() []*migration {
|
||||
newMigration(311, "Add TimeEstimate to Issue table", v1_23.AddTimeEstimateColumnToIssueTable),
|
||||
|
||||
// Gitea 1.23.0-rc0 ends at migration ID number 311 (database version 312)
|
||||
newMigration(312, "Add table issue_dev_link", v1_24.CreateTableIssueDevLink),
|
||||
newMigration(312, "Add DeleteBranchAfterMerge to AutoMerge", v1_24.AddDeleteBranchAfterMergeForAutoMerge),
|
||||
newMigration(313, "Add table issue_dev_link", v1_24.CreateTableIssueDevLink),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ func getRemoteAddress(ownerName, repoName, remoteName string) (string, error) {
|
||||
return "", fmt.Errorf("get remote %s's address of %s/%s failed: %v", remoteName, ownerName, repoName, err)
|
||||
}
|
||||
|
||||
u, err := giturl.Parse(remoteURL)
|
||||
u, err := giturl.ParseGitURL(remoteURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -4,19 +4,18 @@
|
||||
package v1_24 //nolint
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func CreateTableIssueDevLink(x *xorm.Engine) error {
|
||||
type IssueDevLink struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
LinkType int
|
||||
LinkedRepoID int64 `xorm:"INDEX"` // it can link to self repo or other repo
|
||||
LinkIndex string // branch name, pull request number or commit sha
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
return x.Sync(new(IssueDevLink))
|
||||
type pullAutoMerge struct {
|
||||
DeleteBranchAfterMerge bool
|
||||
}
|
||||
|
||||
// TableName return database table name for xorm
|
||||
func (pullAutoMerge) TableName() string {
|
||||
return "pull_auto_merge"
|
||||
}
|
||||
|
||||
func AddDeleteBranchAfterMergeForAutoMerge(x *xorm.Engine) error {
|
||||
return x.Sync(new(pullAutoMerge))
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_24 //nolint
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func CreateTableIssueDevLink(x *xorm.Engine) error {
|
||||
type IssueDevLink struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
LinkType int
|
||||
LinkedRepoID int64 `xorm:"INDEX"` // it can link to self repo or other repo
|
||||
LinkIndex string // branch name, pull request number or commit sha
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||
}
|
||||
return x.Sync(new(IssueDevLink))
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUser_IsOwnedBy(t *testing.T) {
|
||||
@@ -180,9 +181,8 @@ func TestRestrictedUserOrgMembers(t *testing.T) {
|
||||
ID: 29,
|
||||
IsRestricted: true,
|
||||
})
|
||||
if !assert.True(t, restrictedUser.IsRestricted) {
|
||||
return // ensure fixtures return restricted user
|
||||
}
|
||||
// ensure fixtures return restricted user
|
||||
require.True(t, restrictedUser.IsRestricted)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package organization
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type WorktimeSumByRepos struct {
|
||||
RepoName string
|
||||
SumTime int64
|
||||
}
|
||||
|
||||
func GetWorktimeByRepos(org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByRepos, err error) {
|
||||
err = db.GetEngine(db.DefaultContext).
|
||||
Select("repository.name AS repo_name, SUM(tracked_time.time) AS sum_time").
|
||||
Table("tracked_time").
|
||||
Join("INNER", "issue", "tracked_time.issue_id = issue.id").
|
||||
Join("INNER", "repository", "issue.repo_id = repository.id").
|
||||
Where(builder.Eq{"repository.owner_id": org.ID}).
|
||||
And(builder.Eq{"tracked_time.deleted": false}).
|
||||
And(builder.Gte{"tracked_time.created_unix": unitFrom}).
|
||||
And(builder.Lte{"tracked_time.created_unix": unixTo}).
|
||||
GroupBy("repository.name").
|
||||
OrderBy("repository.name").
|
||||
Find(&results)
|
||||
return results, err
|
||||
}
|
||||
|
||||
type WorktimeSumByMilestones struct {
|
||||
RepoName string
|
||||
MilestoneName string
|
||||
MilestoneID int64
|
||||
MilestoneDeadline int64
|
||||
SumTime int64
|
||||
HideRepoName bool
|
||||
}
|
||||
|
||||
func GetWorktimeByMilestones(org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByMilestones, err error) {
|
||||
err = db.GetEngine(db.DefaultContext).
|
||||
Select("repository.name AS repo_name, milestone.name AS milestone_name, milestone.id AS milestone_id, milestone.deadline_unix as milestone_deadline, SUM(tracked_time.time) AS sum_time").
|
||||
Table("tracked_time").
|
||||
Join("INNER", "issue", "tracked_time.issue_id = issue.id").
|
||||
Join("INNER", "repository", "issue.repo_id = repository.id").
|
||||
Join("LEFT", "milestone", "issue.milestone_id = milestone.id").
|
||||
Where(builder.Eq{"repository.owner_id": org.ID}).
|
||||
And(builder.Eq{"tracked_time.deleted": false}).
|
||||
And(builder.Gte{"tracked_time.created_unix": unitFrom}).
|
||||
And(builder.Lte{"tracked_time.created_unix": unixTo}).
|
||||
GroupBy("repository.name, milestone.name, milestone.deadline_unix, milestone.id").
|
||||
OrderBy("repository.name, milestone.deadline_unix, milestone.id").
|
||||
Find(&results)
|
||||
|
||||
// TODO: pgsql: NULL values are sorted last in default ascending order, so we need to sort them manually again.
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
if results[i].RepoName != results[j].RepoName {
|
||||
return results[i].RepoName < results[j].RepoName
|
||||
}
|
||||
if results[i].MilestoneDeadline != results[j].MilestoneDeadline {
|
||||
return results[i].MilestoneDeadline < results[j].MilestoneDeadline
|
||||
}
|
||||
return results[i].MilestoneID < results[j].MilestoneID
|
||||
})
|
||||
|
||||
// Show only the first RepoName, for nicer output.
|
||||
prevRepoName := ""
|
||||
for i := 0; i < len(results); i++ {
|
||||
res := &results[i]
|
||||
res.MilestoneDeadline = 0 // clear the deadline because we do not really need it
|
||||
if prevRepoName == res.RepoName {
|
||||
res.HideRepoName = true
|
||||
}
|
||||
prevRepoName = res.RepoName
|
||||
}
|
||||
return results, err
|
||||
}
|
||||
|
||||
type WorktimeSumByMembers struct {
|
||||
UserName string
|
||||
SumTime int64
|
||||
}
|
||||
|
||||
func GetWorktimeByMembers(org *Organization, unitFrom, unixTo int64) (results []WorktimeSumByMembers, err error) {
|
||||
err = db.GetEngine(db.DefaultContext).
|
||||
Select("`user`.name AS user_name, SUM(tracked_time.time) AS sum_time").
|
||||
Table("tracked_time").
|
||||
Join("INNER", "issue", "tracked_time.issue_id = issue.id").
|
||||
Join("INNER", "repository", "issue.repo_id = repository.id").
|
||||
Join("INNER", "`user`", "tracked_time.user_id = `user`.id").
|
||||
Where(builder.Eq{"repository.owner_id": org.ID}).
|
||||
And(builder.Eq{"tracked_time.deleted": false}).
|
||||
And(builder.Gte{"tracked_time.created_unix": unitFrom}).
|
||||
And(builder.Lte{"tracked_time.created_unix": unixTo}).
|
||||
GroupBy("`user`.name").
|
||||
OrderBy("sum_time DESC").
|
||||
Find(&results)
|
||||
return results, err
|
||||
}
|
||||
@@ -248,6 +248,18 @@ func GetPackageByID(ctx context.Context, packageID int64) (*Package, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// UpdatePackageNameByID updates the package's name, it is only for internal usage, for example: rename some legacy packages
|
||||
func UpdatePackageNameByID(ctx context.Context, ownerID int64, packageType Type, packageID int64, name string) error {
|
||||
var cond builder.Cond = builder.Eq{
|
||||
"package.id": packageID,
|
||||
"package.owner_id": ownerID,
|
||||
"package.type": packageType,
|
||||
"package.is_internal": false,
|
||||
}
|
||||
_, err := db.GetEngine(ctx).Where(cond).Update(&Package{Name: name, LowerName: strings.ToLower(name)})
|
||||
return err
|
||||
}
|
||||
|
||||
// GetPackageByName gets a package by name
|
||||
func GetPackageByName(ctx context.Context, ownerID int64, packageType Type, name string) (*Package, error) {
|
||||
var cond builder.Cond = builder.Eq{
|
||||
|
||||
@@ -175,10 +175,14 @@ func (p *Permission) LogString() string {
|
||||
return fmt.Sprintf(format, args...)
|
||||
}
|
||||
|
||||
func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) {
|
||||
func finalProcessRepoUnitPermission(user *user_model.User, perm *Permission) {
|
||||
if user == nil || user.ID <= 0 {
|
||||
// for anonymous access, it could be:
|
||||
// AccessMode is None or Read, units has repo units, unitModes is nil
|
||||
return
|
||||
}
|
||||
|
||||
// apply everyone access permissions
|
||||
for _, u := range perm.units {
|
||||
if u.EveryoneAccessMode >= perm_model.AccessModeRead && u.EveryoneAccessMode > perm.everyoneAccessMode[u.Type] {
|
||||
if perm.everyoneAccessMode == nil {
|
||||
@@ -187,17 +191,40 @@ func applyEveryoneRepoPermission(user *user_model.User, perm *Permission) {
|
||||
perm.everyoneAccessMode[u.Type] = u.EveryoneAccessMode
|
||||
}
|
||||
}
|
||||
|
||||
if perm.unitsMode == nil {
|
||||
// if unitsMode is not set, then it means that the default p.AccessMode applies to all units
|
||||
return
|
||||
}
|
||||
|
||||
// remove no permission units
|
||||
origPermUnits := perm.units
|
||||
perm.units = make([]*repo_model.RepoUnit, 0, len(perm.units))
|
||||
for _, u := range origPermUnits {
|
||||
shouldKeep := false
|
||||
for t := range perm.unitsMode {
|
||||
if shouldKeep = u.Type == t; shouldKeep {
|
||||
break
|
||||
}
|
||||
}
|
||||
for t := range perm.everyoneAccessMode {
|
||||
if shouldKeep = shouldKeep || u.Type == t; shouldKeep {
|
||||
break
|
||||
}
|
||||
}
|
||||
if shouldKeep {
|
||||
perm.units = append(perm.units, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserRepoPermission returns the user permissions to the repository
|
||||
func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, user *user_model.User) (perm Permission, err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
applyEveryoneRepoPermission(user, &perm)
|
||||
}
|
||||
if log.IsTrace() {
|
||||
log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm)
|
||||
finalProcessRepoUnitPermission(user, &perm)
|
||||
}
|
||||
log.Trace("Permission Loaded for user %-v in repo %-v, permissions: %-+v", user, repo, perm)
|
||||
}()
|
||||
|
||||
if err = repo.LoadUnits(ctx); err != nil {
|
||||
@@ -294,16 +321,6 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
|
||||
}
|
||||
}
|
||||
|
||||
// remove no permission units
|
||||
perm.units = make([]*repo_model.RepoUnit, 0, len(repo.Units))
|
||||
for t := range perm.unitsMode {
|
||||
for _, u := range repo.Units {
|
||||
if u.Type == t {
|
||||
perm.units = append(perm.units, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return perm, err
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestApplyEveryoneRepoPermission(t *testing.T) {
|
||||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
|
||||
},
|
||||
}
|
||||
applyEveryoneRepoPermission(nil, &perm)
|
||||
finalProcessRepoUnitPermission(nil, &perm)
|
||||
assert.False(t, perm.CanRead(unit.TypeWiki))
|
||||
|
||||
perm = Permission{
|
||||
@@ -59,7 +59,7 @@ func TestApplyEveryoneRepoPermission(t *testing.T) {
|
||||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
|
||||
},
|
||||
}
|
||||
applyEveryoneRepoPermission(&user_model.User{ID: 0}, &perm)
|
||||
finalProcessRepoUnitPermission(&user_model.User{ID: 0}, &perm)
|
||||
assert.False(t, perm.CanRead(unit.TypeWiki))
|
||||
|
||||
perm = Permission{
|
||||
@@ -68,7 +68,7 @@ func TestApplyEveryoneRepoPermission(t *testing.T) {
|
||||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
|
||||
},
|
||||
}
|
||||
applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm)
|
||||
finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm)
|
||||
assert.True(t, perm.CanRead(unit.TypeWiki))
|
||||
|
||||
perm = Permission{
|
||||
@@ -77,20 +77,22 @@ func TestApplyEveryoneRepoPermission(t *testing.T) {
|
||||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
|
||||
},
|
||||
}
|
||||
applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm)
|
||||
finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm)
|
||||
// it should work the same as "EveryoneAccessMode: none" because the default AccessMode should be applied to units
|
||||
assert.True(t, perm.CanWrite(unit.TypeWiki))
|
||||
|
||||
perm = Permission{
|
||||
units: []*repo_model.RepoUnit{
|
||||
{Type: unit.TypeCode}, // will be removed
|
||||
{Type: unit.TypeWiki, EveryoneAccessMode: perm_model.AccessModeRead},
|
||||
},
|
||||
unitsMode: map[unit.Type]perm_model.AccessMode{
|
||||
unit.TypeWiki: perm_model.AccessModeWrite,
|
||||
},
|
||||
}
|
||||
applyEveryoneRepoPermission(&user_model.User{ID: 1}, &perm)
|
||||
finalProcessRepoUnitPermission(&user_model.User{ID: 1}, &perm)
|
||||
assert.True(t, perm.CanWrite(unit.TypeWiki))
|
||||
assert.Len(t, perm.units, 1)
|
||||
}
|
||||
|
||||
func TestUnitAccessMode(t *testing.T) {
|
||||
|
||||
@@ -126,6 +126,14 @@ func (p *Project) LoadRepo(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func ProjectLinkForOrg(org *user_model.User, projectID int64) string { //nolint
|
||||
return fmt.Sprintf("%s/-/projects/%d", org.HomeLink(), projectID)
|
||||
}
|
||||
|
||||
func ProjectLinkForRepo(repo *repo_model.Repository, projectID int64) string { //nolint
|
||||
return fmt.Sprintf("%s/projects/%d", repo.Link(), projectID)
|
||||
}
|
||||
|
||||
// Link returns the project's relative URL.
|
||||
func (p *Project) Link(ctx context.Context) string {
|
||||
if p.OwnerID > 0 {
|
||||
@@ -134,7 +142,7 @@ func (p *Project) Link(ctx context.Context) string {
|
||||
log.Error("LoadOwner: %v", err)
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s/-/projects/%d", p.Owner.HomeLink(), p.ID)
|
||||
return ProjectLinkForOrg(p.Owner, p.ID)
|
||||
}
|
||||
if p.RepoID > 0 {
|
||||
err := p.LoadRepo(ctx)
|
||||
@@ -142,7 +150,7 @@ func (p *Project) Link(ctx context.Context) string {
|
||||
log.Error("LoadRepo: %v", err)
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s/projects/%d", p.Repo.Link(), p.ID)
|
||||
return ProjectLinkForRepo(p.Repo, p.ID)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -236,6 +244,10 @@ func GetSearchOrderByBySortType(sortType string) db.SearchOrderBy {
|
||||
return db.SearchOrderByRecentUpdated
|
||||
case "leastupdate":
|
||||
return db.SearchOrderByLeastUpdated
|
||||
case "alphabetically":
|
||||
return "title ASC"
|
||||
case "reversealphabetically":
|
||||
return "title DESC"
|
||||
default:
|
||||
return db.SearchOrderByNewest
|
||||
}
|
||||
|
||||
+14
-12
@@ -15,13 +15,14 @@ import (
|
||||
|
||||
// AutoMerge represents a pull request scheduled for merging when checks succeed
|
||||
type AutoMerge struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
PullID int64 `xorm:"UNIQUE"`
|
||||
DoerID int64 `xorm:"INDEX NOT NULL"`
|
||||
Doer *user_model.User `xorm:"-"`
|
||||
MergeStyle repo_model.MergeStyle `xorm:"varchar(30)"`
|
||||
Message string `xorm:"LONGTEXT"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
PullID int64 `xorm:"UNIQUE"`
|
||||
DoerID int64 `xorm:"INDEX NOT NULL"`
|
||||
Doer *user_model.User `xorm:"-"`
|
||||
MergeStyle repo_model.MergeStyle `xorm:"varchar(30)"`
|
||||
Message string `xorm:"LONGTEXT"`
|
||||
DeleteBranchAfterMerge bool
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
}
|
||||
|
||||
// TableName return database table name for xorm
|
||||
@@ -49,7 +50,7 @@ func IsErrAlreadyScheduledToAutoMerge(err error) bool {
|
||||
}
|
||||
|
||||
// ScheduleAutoMerge schedules a pull request to be merged when all checks succeed
|
||||
func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, style repo_model.MergeStyle, message string) error {
|
||||
func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64, style repo_model.MergeStyle, message string, deleteBranchAfterMerge bool) error {
|
||||
// Check if we already have a merge scheduled for that pull request
|
||||
if exists, _, err := GetScheduledMergeByPullID(ctx, pullID); err != nil {
|
||||
return err
|
||||
@@ -58,10 +59,11 @@ func ScheduleAutoMerge(ctx context.Context, doer *user_model.User, pullID int64,
|
||||
}
|
||||
|
||||
_, err := db.GetEngine(ctx).Insert(&AutoMerge{
|
||||
DoerID: doer.ID,
|
||||
PullID: pullID,
|
||||
MergeStyle: style,
|
||||
Message: message,
|
||||
DoerID: doer.ID,
|
||||
PullID: pullID,
|
||||
MergeStyle: style,
|
||||
Message: message,
|
||||
DeleteBranchAfterMerge: deleteBranchAfterMerge,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -56,16 +56,11 @@ func repoArchiverForRelativePath(relativePath string) (*RepoArchiver, error) {
|
||||
if err != nil {
|
||||
return nil, util.SilentWrap{Message: fmt.Sprintf("invalid storage path: %s", relativePath), Err: util.ErrInvalidArgument}
|
||||
}
|
||||
nameExts := strings.SplitN(parts[2], ".", 2)
|
||||
if len(nameExts) != 2 {
|
||||
commitID, archiveType := git.SplitArchiveNameType(parts[2])
|
||||
if archiveType == git.ArchiveUnknown {
|
||||
return nil, util.SilentWrap{Message: fmt.Sprintf("invalid storage path: %s", relativePath), Err: util.ErrInvalidArgument}
|
||||
}
|
||||
|
||||
return &RepoArchiver{
|
||||
RepoID: repoID,
|
||||
CommitID: parts[1] + nameExts[0],
|
||||
Type: git.ToArchiveType(nameExts[1]),
|
||||
}, nil
|
||||
return &RepoArchiver{RepoID: repoID, CommitID: commitID, Type: archiveType}, nil
|
||||
}
|
||||
|
||||
// GetRepoArchiver get an archiver
|
||||
|
||||
@@ -54,6 +54,7 @@ func UpdateRepoLicenses(ctx context.Context, repo *Repository, commitID string,
|
||||
for _, o := range oldLicenses {
|
||||
// Update already existing license
|
||||
if o.License == license {
|
||||
o.CommitID = commitID
|
||||
if _, err := db.GetEngine(ctx).ID(o.ID).Cols("`commit_id`").Update(o); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+62
-56
@@ -11,14 +11,17 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
giturl "code.gitea.io/gitea/modules/git/url"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
@@ -59,18 +62,30 @@ func (err ErrRepoIsArchived) Error() string {
|
||||
return fmt.Sprintf("%s is archived", err.Repo.LogString())
|
||||
}
|
||||
|
||||
var (
|
||||
reservedRepoNames = []string{".", "..", "-"}
|
||||
reservedRepoPatterns = []string{"*.git", "*.wiki", "*.rss", "*.atom"}
|
||||
)
|
||||
type globalVarsStruct struct {
|
||||
validRepoNamePattern *regexp.Regexp
|
||||
invalidRepoNamePattern *regexp.Regexp
|
||||
reservedRepoNames []string
|
||||
reservedRepoPatterns []string
|
||||
}
|
||||
|
||||
// IsUsableRepoName returns true when repository is usable
|
||||
var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
||||
return &globalVarsStruct{
|
||||
validRepoNamePattern: regexp.MustCompile(`[-.\w]+`),
|
||||
invalidRepoNamePattern: regexp.MustCompile(`[.]{2,}`),
|
||||
reservedRepoNames: []string{".", "..", "-"},
|
||||
reservedRepoPatterns: []string{"*.git", "*.wiki", "*.rss", "*.atom"},
|
||||
}
|
||||
})
|
||||
|
||||
// IsUsableRepoName returns true when name is usable
|
||||
func IsUsableRepoName(name string) error {
|
||||
if db.AlphaDashDotPattern.MatchString(name) {
|
||||
vars := globalVars()
|
||||
if !vars.validRepoNamePattern.MatchString(name) || vars.invalidRepoNamePattern.MatchString(name) {
|
||||
// Note: usually this error is normally caught up earlier in the UI
|
||||
return db.ErrNameCharsNotAllowed{Name: name}
|
||||
}
|
||||
return db.IsUsableName(reservedRepoNames, reservedRepoPatterns, name)
|
||||
return db.IsUsableName(vars.reservedRepoNames, vars.reservedRepoPatterns, name)
|
||||
}
|
||||
|
||||
// TrustModelType defines the types of trust model for this repository
|
||||
@@ -276,6 +291,8 @@ func (repo *Repository) IsBroken() bool {
|
||||
}
|
||||
|
||||
// MarkAsBrokenEmpty marks the repo as broken and empty
|
||||
// FIXME: the status "broken" and "is_empty" were abused,
|
||||
// The code always set them together, no way to distinguish whether a repo is really "empty" or "broken"
|
||||
func (repo *Repository) MarkAsBrokenEmpty() {
|
||||
repo.Status = RepositoryBroken
|
||||
repo.IsEmpty = true
|
||||
@@ -632,14 +649,26 @@ type CloneLink struct {
|
||||
}
|
||||
|
||||
// ComposeHTTPSCloneURL returns HTTPS clone URL based on given owner and repository name.
|
||||
func ComposeHTTPSCloneURL(owner, repo string) string {
|
||||
return fmt.Sprintf("%s%s/%s.git", setting.AppURL, url.PathEscape(owner), url.PathEscape(repo))
|
||||
func ComposeHTTPSCloneURL(ctx context.Context, owner, repo string) string {
|
||||
return fmt.Sprintf("%s%s/%s.git", httplib.GuessCurrentAppURL(ctx), url.PathEscape(owner), url.PathEscape(repo))
|
||||
}
|
||||
|
||||
func ComposeSSHCloneURL(ownerName, repoName string) string {
|
||||
func ComposeSSHCloneURL(doer *user_model.User, ownerName, repoName string) string {
|
||||
sshUser := setting.SSH.User
|
||||
sshDomain := setting.SSH.Domain
|
||||
|
||||
if sshUser == "(DOER_USERNAME)" {
|
||||
// Some users use SSH reverse-proxy and need to use the current signed-in username as the SSH user
|
||||
// to make the SSH reverse-proxy could prepare the user's public keys ahead.
|
||||
// For most cases we have the correct "doer", then use it as the SSH user.
|
||||
// If we can't get the doer, then use the built-in SSH user.
|
||||
if doer != nil {
|
||||
sshUser = doer.Name
|
||||
} else {
|
||||
sshUser = setting.SSH.BuiltinServerUser
|
||||
}
|
||||
}
|
||||
|
||||
// non-standard port, it must use full URI
|
||||
if setting.SSH.Port != 22 {
|
||||
sshHost := net.JoinHostPort(sshDomain, strconv.Itoa(setting.SSH.Port))
|
||||
@@ -657,21 +686,20 @@ func ComposeSSHCloneURL(ownerName, repoName string) string {
|
||||
return fmt.Sprintf("%s@%s:%s/%s.git", sshUser, sshHost, url.PathEscape(ownerName), url.PathEscape(repoName))
|
||||
}
|
||||
|
||||
func (repo *Repository) cloneLink(isWiki bool) *CloneLink {
|
||||
repoName := repo.Name
|
||||
if isWiki {
|
||||
repoName += ".wiki"
|
||||
}
|
||||
|
||||
func (repo *Repository) cloneLink(ctx context.Context, doer *user_model.User, repoPathName string) *CloneLink {
|
||||
cl := new(CloneLink)
|
||||
cl.SSH = ComposeSSHCloneURL(repo.OwnerName, repoName)
|
||||
cl.HTTPS = ComposeHTTPSCloneURL(repo.OwnerName, repoName)
|
||||
cl.SSH = ComposeSSHCloneURL(doer, repo.OwnerName, repoPathName)
|
||||
cl.HTTPS = ComposeHTTPSCloneURL(ctx, repo.OwnerName, repoPathName)
|
||||
return cl
|
||||
}
|
||||
|
||||
// CloneLink returns clone URLs of repository.
|
||||
func (repo *Repository) CloneLink() (cl *CloneLink) {
|
||||
return repo.cloneLink(false)
|
||||
func (repo *Repository) CloneLink(ctx context.Context, doer *user_model.User) (cl *CloneLink) {
|
||||
return repo.cloneLink(ctx, doer, repo.Name)
|
||||
}
|
||||
|
||||
func (repo *Repository) CloneLinkGeneral(ctx context.Context) (cl *CloneLink) {
|
||||
return repo.cloneLink(ctx, nil /* no doer, use a general git user */, repo.Name)
|
||||
}
|
||||
|
||||
// GetOriginalURLHostname returns the hostname of a URL or the URL
|
||||
@@ -767,47 +795,25 @@ func GetRepositoryByName(ctx context.Context, ownerID int64, name string) (*Repo
|
||||
return &repo, err
|
||||
}
|
||||
|
||||
// getRepositoryURLPathSegments returns segments (owner, reponame) extracted from a url
|
||||
func getRepositoryURLPathSegments(repoURL string) []string {
|
||||
if strings.HasPrefix(repoURL, setting.AppURL) {
|
||||
return strings.Split(strings.TrimPrefix(repoURL, setting.AppURL), "/")
|
||||
}
|
||||
|
||||
sshURLVariants := [4]string{
|
||||
setting.SSH.Domain + ":",
|
||||
setting.SSH.User + "@" + setting.SSH.Domain + ":",
|
||||
"git+ssh://" + setting.SSH.Domain + "/",
|
||||
"git+ssh://" + setting.SSH.User + "@" + setting.SSH.Domain + "/",
|
||||
}
|
||||
|
||||
for _, sshURL := range sshURLVariants {
|
||||
if strings.HasPrefix(repoURL, sshURL) {
|
||||
return strings.Split(strings.TrimPrefix(repoURL, sshURL), "/")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRepositoryByURL returns the repository by given url
|
||||
func GetRepositoryByURL(ctx context.Context, repoURL string) (*Repository, error) {
|
||||
// possible urls for git:
|
||||
// https://my.domain/sub-path/<owner>/<repo>.git
|
||||
// https://my.domain/sub-path/<owner>/<repo>
|
||||
// git+ssh://user@my.domain/<owner>/<repo>.git
|
||||
// git+ssh://user@my.domain/<owner>/<repo>
|
||||
// user@my.domain:<owner>/<repo>.git
|
||||
// user@my.domain:<owner>/<repo>
|
||||
|
||||
pathSegments := getRepositoryURLPathSegments(repoURL)
|
||||
|
||||
if len(pathSegments) != 2 {
|
||||
ret, err := giturl.ParseRepositoryURL(ctx, repoURL)
|
||||
if err != nil || ret.OwnerName == "" {
|
||||
return nil, fmt.Errorf("unknown or malformed repository URL")
|
||||
}
|
||||
return GetRepositoryByOwnerAndName(ctx, ret.OwnerName, ret.RepoName)
|
||||
}
|
||||
|
||||
ownerName := pathSegments[0]
|
||||
repoName := strings.TrimSuffix(pathSegments[1], ".git")
|
||||
return GetRepositoryByOwnerAndName(ctx, ownerName, repoName)
|
||||
// GetRepositoryByURLRelax also accepts an SSH clone URL without user part
|
||||
func GetRepositoryByURLRelax(ctx context.Context, repoURL string) (*Repository, error) {
|
||||
if !strings.Contains(repoURL, "://") && !strings.Contains(repoURL, "@") {
|
||||
// convert "example.com:owner/repo" to "@example.com:owner/repo"
|
||||
p1, p2, p3 := strings.Index(repoURL, "."), strings.Index(repoURL, ":"), strings.Index(repoURL, "/")
|
||||
if 0 < p1 && p1 < p2 && p2 < p3 {
|
||||
repoURL = "@" + repoURL
|
||||
}
|
||||
}
|
||||
return GetRepositoryByURL(ctx, repoURL)
|
||||
}
|
||||
|
||||
// GetRepositoryByID returns the repository by given id if exists.
|
||||
|
||||
+48
-44
@@ -16,6 +16,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -132,60 +133,43 @@ func TestGetRepositoryByURL(t *testing.T) {
|
||||
|
||||
t.Run("InvalidPath", func(t *testing.T) {
|
||||
repo, err := GetRepositoryByURL(db.DefaultContext, "something")
|
||||
|
||||
assert.Nil(t, repo)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
testRepo2 := func(t *testing.T, url string) {
|
||||
repo, err := GetRepositoryByURL(db.DefaultContext, url)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, 2, repo.ID)
|
||||
assert.EqualValues(t, 2, repo.OwnerID)
|
||||
}
|
||||
|
||||
t.Run("ValidHttpURL", func(t *testing.T) {
|
||||
test := func(t *testing.T, url string) {
|
||||
repo, err := GetRepositoryByURL(db.DefaultContext, url)
|
||||
|
||||
assert.NotNil(t, repo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, int64(2), repo.ID)
|
||||
assert.Equal(t, int64(2), repo.OwnerID)
|
||||
}
|
||||
|
||||
test(t, "https://try.gitea.io/user2/repo2")
|
||||
test(t, "https://try.gitea.io/user2/repo2.git")
|
||||
testRepo2(t, "https://try.gitea.io/user2/repo2")
|
||||
testRepo2(t, "https://try.gitea.io/user2/repo2.git")
|
||||
})
|
||||
|
||||
t.Run("ValidGitSshURL", func(t *testing.T) {
|
||||
test := func(t *testing.T, url string) {
|
||||
repo, err := GetRepositoryByURL(db.DefaultContext, url)
|
||||
testRepo2(t, "git+ssh://sshuser@try.gitea.io/user2/repo2")
|
||||
testRepo2(t, "git+ssh://sshuser@try.gitea.io/user2/repo2.git")
|
||||
|
||||
assert.NotNil(t, repo)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, int64(2), repo.ID)
|
||||
assert.Equal(t, int64(2), repo.OwnerID)
|
||||
}
|
||||
|
||||
test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2")
|
||||
test(t, "git+ssh://sshuser@try.gitea.io/user2/repo2.git")
|
||||
|
||||
test(t, "git+ssh://try.gitea.io/user2/repo2")
|
||||
test(t, "git+ssh://try.gitea.io/user2/repo2.git")
|
||||
testRepo2(t, "git+ssh://try.gitea.io/user2/repo2")
|
||||
testRepo2(t, "git+ssh://try.gitea.io/user2/repo2.git")
|
||||
})
|
||||
|
||||
t.Run("ValidImplicitSshURL", func(t *testing.T) {
|
||||
test := func(t *testing.T, url string) {
|
||||
repo, err := GetRepositoryByURL(db.DefaultContext, url)
|
||||
|
||||
assert.NotNil(t, repo)
|
||||
assert.NoError(t, err)
|
||||
testRepo2(t, "sshuser@try.gitea.io:user2/repo2")
|
||||
testRepo2(t, "sshuser@try.gitea.io:user2/repo2.git")
|
||||
|
||||
testRelax := func(t *testing.T, url string) {
|
||||
repo, err := GetRepositoryByURLRelax(db.DefaultContext, url)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(2), repo.ID)
|
||||
assert.Equal(t, int64(2), repo.OwnerID)
|
||||
}
|
||||
|
||||
test(t, "sshuser@try.gitea.io:user2/repo2")
|
||||
test(t, "sshuser@try.gitea.io:user2/repo2.git")
|
||||
|
||||
test(t, "try.gitea.io:user2/repo2")
|
||||
test(t, "try.gitea.io:user2/repo2.git")
|
||||
// TODO: it doesn't seem to be common git ssh URL, should we really support this?
|
||||
testRelax(t, "try.gitea.io:user2/repo2")
|
||||
testRelax(t, "try.gitea.io:user2/repo2.git")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -199,21 +183,41 @@ func TestComposeSSHCloneURL(t *testing.T) {
|
||||
setting.SSH.Domain = "domain"
|
||||
setting.SSH.Port = 22
|
||||
setting.Repository.UseCompatSSHURI = false
|
||||
assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||
assert.Equal(t, "git@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
|
||||
setting.Repository.UseCompatSSHURI = true
|
||||
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||
assert.Equal(t, "ssh://git@domain/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
|
||||
// test SSH_DOMAIN while use non-standard SSH port
|
||||
setting.SSH.Port = 123
|
||||
setting.Repository.UseCompatSSHURI = false
|
||||
assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||
assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo"))
|
||||
setting.Repository.UseCompatSSHURI = true
|
||||
assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||
assert.Equal(t, "ssh://git@domain:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo"))
|
||||
|
||||
// test IPv6 SSH_DOMAIN
|
||||
setting.Repository.UseCompatSSHURI = false
|
||||
setting.SSH.Domain = "::1"
|
||||
setting.SSH.Port = 22
|
||||
assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||
assert.Equal(t, "git@[::1]:user/repo.git", ComposeSSHCloneURL(nil, "user", "repo"))
|
||||
setting.SSH.Port = 123
|
||||
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL("user", "repo"))
|
||||
assert.Equal(t, "ssh://git@[::1]:123/user/repo.git", ComposeSSHCloneURL(nil, "user", "repo"))
|
||||
|
||||
setting.SSH.User = "(DOER_USERNAME)"
|
||||
setting.SSH.Domain = "domain"
|
||||
setting.SSH.Port = 22
|
||||
assert.Equal(t, "doer@domain:user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
|
||||
setting.SSH.Port = 123
|
||||
assert.Equal(t, "ssh://doer@domain:123/user/repo.git", ComposeSSHCloneURL(&user_model.User{Name: "doer"}, "user", "repo"))
|
||||
}
|
||||
|
||||
func TestIsUsableRepoName(t *testing.T) {
|
||||
assert.NoError(t, IsUsableRepoName("a"))
|
||||
assert.NoError(t, IsUsableRepoName("-1_."))
|
||||
assert.NoError(t, IsUsableRepoName(".profile"))
|
||||
|
||||
assert.Error(t, IsUsableRepoName("-"))
|
||||
assert.Error(t, IsUsableRepoName("🌞"))
|
||||
assert.Error(t, IsUsableRepoName("the..repo"))
|
||||
assert.Error(t, IsUsableRepoName("foo.wiki"))
|
||||
assert.Error(t, IsUsableRepoName("foo.git"))
|
||||
assert.Error(t, IsUsableRepoName("foo.RSS"))
|
||||
}
|
||||
+52
-15
@@ -68,6 +68,7 @@ type RepoTransfer struct { //nolint
|
||||
RecipientID int64
|
||||
Recipient *user_model.User `xorm:"-"`
|
||||
RepoID int64
|
||||
Repo *Repository `xorm:"-"`
|
||||
TeamIDs []int64
|
||||
Teams []*organization.Team `xorm:"-"`
|
||||
|
||||
@@ -79,48 +80,65 @@ func init() {
|
||||
db.RegisterModel(new(RepoTransfer))
|
||||
}
|
||||
|
||||
// LoadAttributes fetches the transfer recipient from the database
|
||||
func (r *RepoTransfer) LoadAttributes(ctx context.Context) error {
|
||||
func (r *RepoTransfer) LoadRecipient(ctx context.Context) error {
|
||||
if r.Recipient == nil {
|
||||
u, err := user_model.GetUserByID(ctx, r.RecipientID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Recipient = u
|
||||
}
|
||||
|
||||
if r.Recipient.IsOrganization() && len(r.TeamIDs) != len(r.Teams) {
|
||||
for _, v := range r.TeamIDs {
|
||||
team, err := organization.GetTeamByID(ctx, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if team.OrgID != r.Recipient.ID {
|
||||
return fmt.Errorf("team %d belongs not to org %d", v, r.Recipient.ID)
|
||||
}
|
||||
func (r *RepoTransfer) LoadRepo(ctx context.Context) error {
|
||||
if r.Repo == nil {
|
||||
repo, err := GetRepositoryByID(ctx, r.RepoID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Repo = repo
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadAttributes fetches the transfer recipient from the database
|
||||
func (r *RepoTransfer) LoadAttributes(ctx context.Context) error {
|
||||
if err := r.LoadRecipient(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Recipient.IsOrganization() && r.Teams == nil {
|
||||
teamsMap, err := organization.GetTeamsByIDs(ctx, r.TeamIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, team := range teamsMap {
|
||||
r.Teams = append(r.Teams, team)
|
||||
}
|
||||
}
|
||||
|
||||
if err := r.LoadRepo(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Doer == nil {
|
||||
u, err := user_model.GetUserByID(ctx, r.DoerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Doer = u
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanUserAcceptTransfer checks if the user has the rights to accept/decline a repo transfer.
|
||||
// CanUserAcceptOrRejectTransfer checks if the user has the rights to accept/decline a repo transfer.
|
||||
// For user, it checks if it's himself
|
||||
// For organizations, it checks if the user is able to create repos
|
||||
func (r *RepoTransfer) CanUserAcceptTransfer(ctx context.Context, u *user_model.User) bool {
|
||||
func (r *RepoTransfer) CanUserAcceptOrRejectTransfer(ctx context.Context, u *user_model.User) bool {
|
||||
if err := r.LoadAttributes(ctx); err != nil {
|
||||
log.Error("LoadAttributes: %v", err)
|
||||
return false
|
||||
@@ -166,6 +184,10 @@ func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryT
|
||||
Find(&transfers)
|
||||
}
|
||||
|
||||
func IsRepositoryTransferExist(ctx context.Context, repoID int64) (bool, error) {
|
||||
return db.GetEngine(ctx).Where("repo_id = ?", repoID).Exist(new(RepoTransfer))
|
||||
}
|
||||
|
||||
// GetPendingRepositoryTransfer fetches the most recent and ongoing transfer
|
||||
// process for the repository
|
||||
func GetPendingRepositoryTransfer(ctx context.Context, repo *Repository) (*RepoTransfer, error) {
|
||||
@@ -206,11 +228,26 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := user_model.GetUserByID(ctx, newOwner.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure repo is ready to transfer
|
||||
if err := TestRepositoryReadyForTransfer(repo.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
exist, err := IsRepositoryTransferExist(ctx, repo.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
return ErrRepoTransferInProgress{
|
||||
Uname: repo.Owner.LowerName,
|
||||
Name: repo.Name,
|
||||
}
|
||||
}
|
||||
|
||||
repo.Status = RepositoryPendingTransfer
|
||||
if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil {
|
||||
return err
|
||||
|
||||
@@ -46,6 +46,12 @@ func UpdateRepositoryCols(ctx context.Context, repo *Repository, cols ...string)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateRepositoryColsNoAutoTime updates repository's columns and but applies time change automatically
|
||||
func UpdateRepositoryColsNoAutoTime(ctx context.Context, repo *Repository, cols ...string) error {
|
||||
_, err := db.GetEngine(ctx).ID(repo.ID).Cols(cols...).NoAutoTime().Update(repo)
|
||||
return err
|
||||
}
|
||||
|
||||
// ErrReachLimitOfRepo represents a "ReachLimitOfRepo" kind of error.
|
||||
type ErrReachLimitOfRepo struct {
|
||||
Limit int
|
||||
|
||||
+3
-2
@@ -5,6 +5,7 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -72,8 +73,8 @@ func (err ErrWikiInvalidFileName) Unwrap() error {
|
||||
}
|
||||
|
||||
// WikiCloneLink returns clone URLs of repository wiki.
|
||||
func (repo *Repository) WikiCloneLink() *CloneLink {
|
||||
return repo.cloneLink(true)
|
||||
func (repo *Repository) WikiCloneLink(ctx context.Context, doer *user_model.User) *CloneLink {
|
||||
return repo.cloneLink(ctx, doer, repo.Name+".wiki")
|
||||
}
|
||||
|
||||
// WikiPath returns wiki data path by given user and repository name.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package repo_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -18,7 +19,7 @@ func TestRepository_WikiCloneLink(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
cloneLink := repo.WikiCloneLink()
|
||||
cloneLink := repo.WikiCloneLink(context.Background(), nil)
|
||||
assert.Equal(t, "ssh://sshuser@try.gitea.io:3000/user2/repo1.wiki.git", cloneLink.SSH)
|
||||
assert.Equal(t, "https://try.gitea.io/user2/repo1.wiki.git", cloneLink.HTTPS)
|
||||
}
|
||||
|
||||
@@ -45,8 +45,6 @@ func TestCreateRepositoryNotice(t *testing.T) {
|
||||
unittest.AssertExistsAndLoadBean(t, noticeBean)
|
||||
}
|
||||
|
||||
// TODO TestRemoveAllWithNotice
|
||||
|
||||
func TestCountNotices(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
assert.Equal(t, int64(3), system.CountNotices(db.DefaultContext))
|
||||
|
||||
+21
-35
@@ -11,35 +11,13 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// Copy copies file from source to target path.
|
||||
func Copy(src, dest string) error {
|
||||
// Gather file information to set back later.
|
||||
si, err := os.Lstat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle symbolic link.
|
||||
if si.Mode()&os.ModeSymlink != 0 {
|
||||
target, err := os.Readlink(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// NOTE: os.Chmod and os.Chtimes don't recognize symbolic link,
|
||||
// which will lead "no such file or directory" error.
|
||||
return os.Symlink(target, dest)
|
||||
}
|
||||
|
||||
return util.CopyFile(src, dest)
|
||||
}
|
||||
|
||||
// Sync synchronizes the two files. This is skipped if both files
|
||||
// SyncFile synchronizes the two files. This is skipped if both files
|
||||
// exist and the size, modtime, and mode match.
|
||||
func Sync(srcPath, destPath string) error {
|
||||
func SyncFile(srcPath, destPath string) error {
|
||||
dest, err := os.Stat(destPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return Copy(srcPath, destPath)
|
||||
return util.CopyFile(srcPath, destPath)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -55,7 +33,7 @@ func Sync(srcPath, destPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Copy(srcPath, destPath)
|
||||
return util.CopyFile(srcPath, destPath)
|
||||
}
|
||||
|
||||
// SyncDirs synchronizes files recursively from source to target directory.
|
||||
@@ -66,37 +44,45 @@ func SyncDirs(srcPath, destPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// the keep file is used to keep the directory in a git repository, it doesn't need to be synced
|
||||
// and go-git doesn't work with the ".keep" file (it would report errors like "ref is empty")
|
||||
const keepFile = ".keep"
|
||||
|
||||
// find and delete all untracked files
|
||||
destFiles, err := util.StatDir(destPath, true)
|
||||
destFiles, err := util.ListDirRecursively(destPath, &util.ListDirOptions{IncludeDir: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, destFile := range destFiles {
|
||||
destFilePath := filepath.Join(destPath, destFile)
|
||||
shouldRemove := filepath.Base(destFilePath) == keepFile
|
||||
if _, err = os.Stat(filepath.Join(srcPath, destFile)); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// if src file does not exist, remove dest file
|
||||
if err = os.RemoveAll(destFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
shouldRemove = true
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// if src file does not exist, remove dest file
|
||||
if shouldRemove {
|
||||
if err = os.RemoveAll(destFilePath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sync src files to dest
|
||||
srcFiles, err := util.StatDir(srcPath, true)
|
||||
srcFiles, err := util.ListDirRecursively(srcPath, &util.ListDirOptions{IncludeDir: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, srcFile := range srcFiles {
|
||||
destFilePath := filepath.Join(destPath, srcFile)
|
||||
// util.StatDir appends a slash to the directory name
|
||||
// util.ListDirRecursively appends a slash to the directory name
|
||||
if strings.HasSuffix(srcFile, "/") {
|
||||
err = os.MkdirAll(destFilePath, os.ModePerm)
|
||||
} else {
|
||||
err = Sync(filepath.Join(srcPath, srcFile), destFilePath)
|
||||
} else if filepath.Base(destFilePath) != keepFile {
|
||||
err = SyncFile(filepath.Join(srcPath, srcFile), destFilePath)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -59,6 +59,7 @@ func InitSettings() {
|
||||
_ = hash.Register("dummy", hash.NewDummyHasher)
|
||||
|
||||
setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
|
||||
setting.InitGiteaEnvVarsForTesting()
|
||||
}
|
||||
|
||||
// TestOptions represents test options
|
||||
@@ -83,6 +84,7 @@ func MainTest(m *testing.M, testOptsArg ...*TestOptions) {
|
||||
|
||||
setting.IsInTesting = true
|
||||
setting.AppURL = "https://try.gitea.io/"
|
||||
setting.Domain = "try.gitea.io"
|
||||
setting.RunUser = "runuser"
|
||||
setting.SSH.User = "sshuser"
|
||||
setting.SSH.BuiltinServerUser = "builtinuser"
|
||||
|
||||
+12
-9
@@ -38,27 +38,30 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
|
||||
|
||||
u.Avatar = avatars.HashEmail(seed)
|
||||
|
||||
// Don't share the images so that we can delete them easily
|
||||
if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
|
||||
if err := png.Encode(w, img); err != nil {
|
||||
log.Error("Encode: %v", err)
|
||||
_, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
|
||||
if err != nil {
|
||||
// If unable to Stat the avatar file (usually it means non-existing), then try to save a new one
|
||||
// Don't share the images so that we can delete them easily
|
||||
if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error {
|
||||
if err := png.Encode(w, img); err != nil {
|
||||
log.Error("Encode: %v", err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to save avatar %s: %w", u.CustomAvatarRelativePath(), err)
|
||||
}
|
||||
return err
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err)
|
||||
}
|
||||
|
||||
if _, err := db.GetEngine(ctx).ID(u.ID).Cols("avatar").Update(u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("New random avatar created: %d", u.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
|
||||
func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
|
||||
if u.IsGhost() {
|
||||
if u.IsGhost() || u.IsGiteaActions() {
|
||||
return avatars.DefaultAvatarLink()
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,19 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUserAvatarLink(t *testing.T) {
|
||||
@@ -26,3 +32,37 @@ func TestUserAvatarLink(t *testing.T) {
|
||||
link = u.AvatarLink(db.DefaultContext)
|
||||
assert.Equal(t, "https://localhost/sub-path/avatars/avatar.png", link)
|
||||
}
|
||||
|
||||
func TestUserAvatarGenerate(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
var err error
|
||||
tmpDir := t.TempDir()
|
||||
storage.Avatars, err = storage.NewLocalStorage(context.Background(), &setting.Storage{Path: tmpDir})
|
||||
require.NoError(t, err)
|
||||
|
||||
u := unittest.AssertExistsAndLoadBean(t, &User{ID: 2})
|
||||
|
||||
// there was no avatar, generate a new one
|
||||
assert.Empty(t, u.Avatar)
|
||||
err = GenerateRandomAvatar(db.DefaultContext, u)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, u.Avatar)
|
||||
|
||||
// make sure the generated one exists
|
||||
oldAvatarPath := u.CustomAvatarRelativePath()
|
||||
_, err = storage.Avatars.Stat(u.CustomAvatarRelativePath())
|
||||
require.NoError(t, err)
|
||||
// and try to change its content
|
||||
_, err = storage.Avatars.Save(u.CustomAvatarRelativePath(), strings.NewReader("abcd"), 4)
|
||||
require.NoError(t, err)
|
||||
|
||||
// try to generate again
|
||||
err = GenerateRandomAvatar(db.DefaultContext, u)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, oldAvatarPath, u.CustomAvatarRelativePath())
|
||||
f, err := storage.Avatars.Open(u.CustomAvatarRelativePath())
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
content, _ := io.ReadAll(f)
|
||||
assert.Equal(t, "abcd", string(content))
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -153,8 +152,6 @@ func UpdateEmailAddress(ctx context.Context, email *EmailAddress) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||
|
||||
// ValidateEmail check if email is a valid & allowed address
|
||||
func ValidateEmail(email string) error {
|
||||
if err := validateEmailBasic(email); err != nil {
|
||||
@@ -514,7 +511,7 @@ func validateEmailBasic(email string) error {
|
||||
return ErrEmailInvalid{email}
|
||||
}
|
||||
|
||||
if !emailRegexp.MatchString(email) {
|
||||
if !globalVars().emailRegexp.MatchString(email) {
|
||||
return ErrEmailCharIsNotSupported{email}
|
||||
}
|
||||
|
||||
@@ -545,3 +542,13 @@ func IsEmailDomainAllowed(email string) bool {
|
||||
|
||||
return validation.IsEmailDomainListed(setting.Service.EmailDomainAllowList, email)
|
||||
}
|
||||
|
||||
func GetActivatedEmailAddresses(ctx context.Context, uid int64) ([]string, error) {
|
||||
emails := make([]string, 0, 2)
|
||||
if err := db.GetEngine(ctx).Table("email_address").Select("email").
|
||||
Where("uid=? AND is_activated=?", uid, true).Asc("id").
|
||||
Find(&emails); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return emails, nil
|
||||
}
|
||||
@@ -11,9 +11,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// ErrOpenIDNotExist openid is not known
|
||||
var ErrOpenIDNotExist = util.NewNotExistErrorf("OpenID is unknown")
|
||||
|
||||
// UserOpenID is the list of all OpenID identities of a user.
|
||||
// Since this is a middle table, name it OpenID is not suitable, so we ignore the lint here
|
||||
type UserOpenID struct { //revive:disable-line:exported
|
||||
@@ -99,7 +96,7 @@ func DeleteUserOpenID(ctx context.Context, openid *UserOpenID) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
} else if deleted != 1 {
|
||||
return ErrOpenIDNotExist
|
||||
return util.NewNotExistErrorf("OpenID is unknown")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetUserOpenIDs(t *testing.T) {
|
||||
@@ -34,30 +35,23 @@ func TestGetUserOpenIDs(t *testing.T) {
|
||||
func TestToggleUserOpenIDVisibility(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
oids, err := user_model.GetUserOpenIDs(db.DefaultContext, int64(2))
|
||||
if !assert.NoError(t, err) || !assert.Len(t, oids, 1) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Len(t, oids, 1)
|
||||
assert.True(t, oids[0].Show)
|
||||
|
||||
err = user_model.ToggleUserOpenIDVisibility(db.DefaultContext, oids[0].ID)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
oids, err = user_model.GetUserOpenIDs(db.DefaultContext, int64(2))
|
||||
if !assert.NoError(t, err) || !assert.Len(t, oids, 1) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Len(t, oids, 1)
|
||||
|
||||
assert.False(t, oids[0].Show)
|
||||
err = user_model.ToggleUserOpenIDVisibility(db.DefaultContext, oids[0].ID)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
oids, err = user_model.GetUserOpenIDs(db.DefaultContext, int64(2))
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
if assert.Len(t, oids, 1) {
|
||||
assert.True(t, oids[0].Show)
|
||||
}
|
||||
|
||||
+73
-63
@@ -14,6 +14,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
@@ -213,7 +214,7 @@ func (u *User) GetPlaceholderEmail() string {
|
||||
return fmt.Sprintf("%s@%s", u.LowerName, setting.Service.NoReplyAddress)
|
||||
}
|
||||
|
||||
// GetEmail returns an noreply email, if the user has set to keep his
|
||||
// GetEmail returns a noreply email, if the user has set to keep his
|
||||
// email address private, otherwise the primary email address.
|
||||
func (u *User) GetEmail() string {
|
||||
if u.KeepEmailPrivate {
|
||||
@@ -384,11 +385,12 @@ func (u *User) ValidatePassword(passwd string) bool {
|
||||
}
|
||||
|
||||
// IsPasswordSet checks if the password is set or left empty
|
||||
// TODO: It's better to clarify the "password" behavior for different types (individual, bot)
|
||||
func (u *User) IsPasswordSet() bool {
|
||||
return len(u.Passwd) != 0
|
||||
return u.Passwd != ""
|
||||
}
|
||||
|
||||
// IsOrganization returns true if user is actually a organization.
|
||||
// IsOrganization returns true if user is actually an organization.
|
||||
func (u *User) IsOrganization() bool {
|
||||
return u.Type == UserTypeOrganization
|
||||
}
|
||||
@@ -398,13 +400,14 @@ func (u *User) IsIndividual() bool {
|
||||
return u.Type == UserTypeIndividual
|
||||
}
|
||||
|
||||
func (u *User) IsUser() bool {
|
||||
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
|
||||
// IsTypeBot returns whether the user is of type bot
|
||||
func (u *User) IsTypeBot() bool {
|
||||
return u.Type == UserTypeBot
|
||||
}
|
||||
|
||||
// IsBot returns whether or not the user is of type bot
|
||||
func (u *User) IsBot() bool {
|
||||
return u.Type == UserTypeBot
|
||||
// IsTokenAccessAllowed returns whether the user is an individual or a bot (which allows for token access)
|
||||
func (u *User) IsTokenAccessAllowed() bool {
|
||||
return u.Type == UserTypeIndividual || u.Type == UserTypeBot
|
||||
}
|
||||
|
||||
// DisplayName returns full name if it's not empty,
|
||||
@@ -417,19 +420,9 @@ func (u *User) DisplayName() string {
|
||||
return u.Name
|
||||
}
|
||||
|
||||
var emailToReplacer = strings.NewReplacer(
|
||||
"\n", "",
|
||||
"\r", "",
|
||||
"<", "",
|
||||
">", "",
|
||||
",", "",
|
||||
":", "",
|
||||
";", "",
|
||||
)
|
||||
|
||||
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
|
||||
func (u *User) EmailTo() string {
|
||||
sanitizedDisplayName := emailToReplacer.Replace(u.DisplayName())
|
||||
sanitizedDisplayName := globalVars().emailToReplacer.Replace(u.DisplayName())
|
||||
|
||||
// should be an edge case but nice to have
|
||||
if sanitizedDisplayName == u.Email {
|
||||
@@ -502,10 +495,10 @@ func (u *User) IsMailable() bool {
|
||||
return u.IsActive
|
||||
}
|
||||
|
||||
// IsUserExist checks if given user name exist,
|
||||
// the user name should be noncased unique.
|
||||
// IsUserExist checks if given username exist,
|
||||
// the username should be non-cased unique.
|
||||
// If uid is presented, then check will rule out that one,
|
||||
// it is used when update a user name in settings page.
|
||||
// it is used when update a username in settings page.
|
||||
func IsUserExist(ctx context.Context, uid int64, name string) (bool, error) {
|
||||
if len(name) == 0 {
|
||||
return false, nil
|
||||
@@ -515,7 +508,7 @@ func IsUserExist(ctx context.Context, uid int64, name string) (bool, error) {
|
||||
Get(&User{LowerName: strings.ToLower(name)})
|
||||
}
|
||||
|
||||
// Note: As of the beginning of 2022, it is recommended to use at least
|
||||
// SaltByteLength as of the beginning of 2022, it is recommended to use at least
|
||||
// 64 bits of salt, but NIST is already recommending to use to 128 bits.
|
||||
// (16 bytes = 16 * 8 = 128 bits)
|
||||
const SaltByteLength = 16
|
||||
@@ -526,28 +519,58 @@ func GetUserSalt() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Returns a 32 bytes long string.
|
||||
// Returns a 32-byte long string.
|
||||
return hex.EncodeToString(rBytes), nil
|
||||
}
|
||||
|
||||
// Note: The set of characters here can safely expand without a breaking change,
|
||||
// but characters removed from this set can cause user account linking to break
|
||||
var (
|
||||
customCharsReplacement = strings.NewReplacer("Æ", "AE")
|
||||
removeCharsRE = regexp.MustCompile("['`´]")
|
||||
transformDiacritics = transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC)
|
||||
replaceCharsHyphenRE = regexp.MustCompile(`[\s~+]`)
|
||||
)
|
||||
type globalVarsStruct struct {
|
||||
customCharsReplacement *strings.Replacer
|
||||
removeCharsRE *regexp.Regexp
|
||||
transformDiacritics transform.Transformer
|
||||
replaceCharsHyphenRE *regexp.Regexp
|
||||
emailToReplacer *strings.Replacer
|
||||
emailRegexp *regexp.Regexp
|
||||
systemUserNewFuncs map[int64]func() *User
|
||||
}
|
||||
|
||||
var globalVars = sync.OnceValue(func() *globalVarsStruct {
|
||||
return &globalVarsStruct{
|
||||
// Note: The set of characters here can safely expand without a breaking change,
|
||||
// but characters removed from this set can cause user account linking to break
|
||||
customCharsReplacement: strings.NewReplacer("Æ", "AE"),
|
||||
|
||||
removeCharsRE: regexp.MustCompile("['`´]"),
|
||||
transformDiacritics: transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC),
|
||||
replaceCharsHyphenRE: regexp.MustCompile(`[\s~+]`),
|
||||
|
||||
emailToReplacer: strings.NewReplacer(
|
||||
"\n", "",
|
||||
"\r", "",
|
||||
"<", "",
|
||||
">", "",
|
||||
",", "",
|
||||
":", "",
|
||||
";", "",
|
||||
),
|
||||
emailRegexp: regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"),
|
||||
|
||||
systemUserNewFuncs: map[int64]func() *User{
|
||||
GhostUserID: NewGhostUser,
|
||||
ActionsUserID: NewActionsUser,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// NormalizeUserName only takes the name part if it is an email address, transforms it diacritics to ASCII characters.
|
||||
// It returns a string with the single-quotes removed, and any other non-supported username characters are replaced with a `-` character
|
||||
func NormalizeUserName(s string) (string, error) {
|
||||
vars := globalVars()
|
||||
s, _, _ = strings.Cut(s, "@")
|
||||
strDiacriticsRemoved, n, err := transform.String(transformDiacritics, customCharsReplacement.Replace(s))
|
||||
strDiacriticsRemoved, n, err := transform.String(vars.transformDiacritics, vars.customCharsReplacement.Replace(s))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to normalize the string of provided username %q at position %d", s, n)
|
||||
}
|
||||
return replaceCharsHyphenRE.ReplaceAllLiteralString(removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
|
||||
return vars.replaceCharsHyphenRE.ReplaceAllLiteralString(vars.removeCharsRE.ReplaceAllLiteralString(strDiacriticsRemoved, ""), "-"), nil
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -963,30 +986,28 @@ func GetUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
return users, err
|
||||
}
|
||||
|
||||
// GetPossibleUserByID returns the user if id > 0 or return system usrs if id < 0
|
||||
// GetPossibleUserByID returns the user if id > 0 or returns system user if id < 0
|
||||
func GetPossibleUserByID(ctx context.Context, id int64) (*User, error) {
|
||||
switch id {
|
||||
case GhostUserID:
|
||||
return NewGhostUser(), nil
|
||||
case ActionsUserID:
|
||||
return NewActionsUser(), nil
|
||||
case 0:
|
||||
if id < 0 {
|
||||
if newFunc, ok := globalVars().systemUserNewFuncs[id]; ok {
|
||||
return newFunc(), nil
|
||||
}
|
||||
return nil, ErrUserNotExist{UID: id}
|
||||
} else if id == 0 {
|
||||
return nil, ErrUserNotExist{}
|
||||
default:
|
||||
return GetUserByID(ctx, id)
|
||||
}
|
||||
return GetUserByID(ctx, id)
|
||||
}
|
||||
|
||||
// GetPossibleUserByIDs returns the users if id > 0 or return system users if id < 0
|
||||
// GetPossibleUserByIDs returns the users if id > 0 or returns system users if id < 0
|
||||
func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
uniqueIDs := container.SetOf(ids...)
|
||||
users := make([]*User, 0, len(ids))
|
||||
_ = uniqueIDs.Remove(0)
|
||||
if uniqueIDs.Remove(GhostUserID) {
|
||||
users = append(users, NewGhostUser())
|
||||
}
|
||||
if uniqueIDs.Remove(ActionsUserID) {
|
||||
users = append(users, NewActionsUser())
|
||||
for systemUID, newFunc := range globalVars().systemUserNewFuncs {
|
||||
if uniqueIDs.Remove(systemUID) {
|
||||
users = append(users, newFunc())
|
||||
}
|
||||
}
|
||||
res, err := GetUserByIDs(ctx, uniqueIDs.Values())
|
||||
if err != nil {
|
||||
@@ -996,7 +1017,7 @@ func GetPossibleUserByIDs(ctx context.Context, ids []int64) ([]*User, error) {
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// GetUserByNameCtx returns user by given name.
|
||||
// GetUserByName returns user by given name.
|
||||
func GetUserByName(ctx context.Context, name string) (*User, error) {
|
||||
if len(name) == 0 {
|
||||
return nil, ErrUserNotExist{Name: name}
|
||||
@@ -1027,8 +1048,8 @@ func GetUserEmailsByNames(ctx context.Context, names []string) []string {
|
||||
return mails
|
||||
}
|
||||
|
||||
// GetMaileableUsersByIDs gets users from ids, but only if they can receive mails
|
||||
func GetMaileableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([]*User, error) {
|
||||
// GetMailableUsersByIDs gets users from ids, but only if they can receive mails
|
||||
func GetMailableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([]*User, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1053,17 +1074,6 @@ func GetMaileableUsersByIDs(ctx context.Context, ids []int64, isMention bool) ([
|
||||
Find(&ous)
|
||||
}
|
||||
|
||||
// GetUserNamesByIDs returns usernames for all resolved users from a list of Ids.
|
||||
func GetUserNamesByIDs(ctx context.Context, ids []int64) ([]string, error) {
|
||||
unames := make([]string, 0, len(ids))
|
||||
err := db.GetEngine(ctx).In("id", ids).
|
||||
Table("user").
|
||||
Asc("name").
|
||||
Cols("name").
|
||||
Find(&unames)
|
||||
return unames, err
|
||||
}
|
||||
|
||||
// GetUserNameByID returns username for the id
|
||||
func GetUserNameByID(ctx context.Context, id int64) (string, error) {
|
||||
var name string
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
)
|
||||
|
||||
func GetUsersMapByIDs(ctx context.Context, userIDs []int64) (map[int64]*User, error) {
|
||||
userMaps := make(map[int64]*User, len(userIDs))
|
||||
left := len(userIDs)
|
||||
for left > 0 {
|
||||
limit := db.DefaultMaxInSize
|
||||
if left < limit {
|
||||
limit = left
|
||||
}
|
||||
err := db.GetEngine(ctx).
|
||||
In("id", userIDs[:limit]).
|
||||
Find(&userMaps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
left -= limit
|
||||
userIDs = userIDs[limit:]
|
||||
}
|
||||
return userMaps, nil
|
||||
}
|
||||
|
||||
func GetPossibleUserFromMap(userID int64, usererMaps map[int64]*User) *User {
|
||||
switch userID {
|
||||
case GhostUserID:
|
||||
return NewGhostUser()
|
||||
case ActionsUserID:
|
||||
return NewActionsUser()
|
||||
case 0:
|
||||
return nil
|
||||
default:
|
||||
user, ok := usererMaps[userID]
|
||||
if !ok {
|
||||
return NewGhostUser()
|
||||
}
|
||||
return user
|
||||
}
|
||||
}
|
||||
+28
-21
@@ -10,9 +10,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
GhostUserID = -1
|
||||
GhostUserName = "Ghost"
|
||||
GhostUserLowerName = "ghost"
|
||||
GhostUserID = -1
|
||||
GhostUserName = "Ghost"
|
||||
)
|
||||
|
||||
// NewGhostUser creates and returns a fake user for someone has deleted their account.
|
||||
@@ -20,10 +19,14 @@ func NewGhostUser() *User {
|
||||
return &User{
|
||||
ID: GhostUserID,
|
||||
Name: GhostUserName,
|
||||
LowerName: GhostUserLowerName,
|
||||
LowerName: strings.ToLower(GhostUserName),
|
||||
}
|
||||
}
|
||||
|
||||
func IsGhostUserName(name string) bool {
|
||||
return strings.EqualFold(name, GhostUserName)
|
||||
}
|
||||
|
||||
// IsGhost check if user is fake user for a deleted account
|
||||
func (u *User) IsGhost() bool {
|
||||
if u == nil {
|
||||
@@ -32,22 +35,16 @@ func (u *User) IsGhost() bool {
|
||||
return u.ID == GhostUserID && u.Name == GhostUserName
|
||||
}
|
||||
|
||||
// NewReplaceUser creates and returns a fake user for external user
|
||||
func NewReplaceUser(name string) *User {
|
||||
return &User{
|
||||
ID: 0,
|
||||
Name: name,
|
||||
LowerName: strings.ToLower(name),
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
ActionsUserID = -2
|
||||
ActionsUserName = "gitea-actions"
|
||||
ActionsFullName = "Gitea Actions"
|
||||
ActionsEmail = "teabot@gitea.io"
|
||||
ActionsUserID = -2
|
||||
ActionsUserName = "gitea-actions"
|
||||
ActionsUserEmail = "teabot@gitea.io"
|
||||
)
|
||||
|
||||
func IsGiteaActionsUserName(name string) bool {
|
||||
return strings.EqualFold(name, ActionsUserName)
|
||||
}
|
||||
|
||||
// NewActionsUser creates and returns a fake user for running the actions.
|
||||
func NewActionsUser() *User {
|
||||
return &User{
|
||||
@@ -55,16 +52,26 @@ func NewActionsUser() *User {
|
||||
Name: ActionsUserName,
|
||||
LowerName: ActionsUserName,
|
||||
IsActive: true,
|
||||
FullName: ActionsFullName,
|
||||
Email: ActionsEmail,
|
||||
FullName: "Gitea Actions",
|
||||
Email: ActionsUserEmail,
|
||||
KeepEmailPrivate: true,
|
||||
LoginName: ActionsUserName,
|
||||
Type: UserTypeIndividual,
|
||||
Type: UserTypeBot,
|
||||
AllowCreateOrganization: true,
|
||||
Visibility: structs.VisibleTypePublic,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) IsActions() bool {
|
||||
func (u *User) IsGiteaActions() bool {
|
||||
return u != nil && u.ID == ActionsUserID
|
||||
}
|
||||
|
||||
func GetSystemUserByName(name string) *User {
|
||||
if IsGhostUserName(name) {
|
||||
return NewGhostUser()
|
||||
}
|
||||
if IsGiteaActionsUserName(name) {
|
||||
return NewActionsUser()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSystemUser(t *testing.T) {
|
||||
u, err := GetPossibleUserByID(db.DefaultContext, -1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Ghost", u.Name)
|
||||
assert.Equal(t, "ghost", u.LowerName)
|
||||
assert.True(t, u.IsGhost())
|
||||
assert.True(t, IsGhostUserName("gHost"))
|
||||
|
||||
u, err = GetPossibleUserByID(db.DefaultContext, -2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "gitea-actions", u.Name)
|
||||
assert.Equal(t, "gitea-actions", u.LowerName)
|
||||
assert.True(t, u.IsGiteaActions())
|
||||
assert.True(t, IsGiteaActionsUserName("Gitea-actionS"))
|
||||
|
||||
_, err = GetPossibleUserByID(db.DefaultContext, -3)
|
||||
require.Error(t, err)
|
||||
}
|
||||
@@ -25,6 +25,21 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsUsableUsername(t *testing.T) {
|
||||
assert.NoError(t, user_model.IsUsableUsername("a"))
|
||||
assert.NoError(t, user_model.IsUsableUsername("foo.wiki"))
|
||||
assert.NoError(t, user_model.IsUsableUsername("foo.git"))
|
||||
|
||||
assert.Error(t, user_model.IsUsableUsername("a--b"))
|
||||
assert.Error(t, user_model.IsUsableUsername("-1_."))
|
||||
assert.Error(t, user_model.IsUsableUsername(".profile"))
|
||||
assert.Error(t, user_model.IsUsableUsername("-"))
|
||||
assert.Error(t, user_model.IsUsableUsername("🌞"))
|
||||
assert.Error(t, user_model.IsUsableUsername("the..repo"))
|
||||
assert.Error(t, user_model.IsUsableUsername("foo.RSS"))
|
||||
assert.Error(t, user_model.IsUsableUsername("foo.PnG"))
|
||||
}
|
||||
|
||||
func TestOAuth2Application_LoadUser(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
app := unittest.AssertExistsAndLoadBean(t, &auth.OAuth2Application{ID: 1})
|
||||
@@ -318,14 +333,14 @@ func TestGetUserIDsByNames(t *testing.T) {
|
||||
func TestGetMaileableUsersByIDs(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
results, err := user_model.GetMaileableUsersByIDs(db.DefaultContext, []int64{1, 4}, false)
|
||||
results, err := user_model.GetMailableUsersByIDs(db.DefaultContext, []int64{1, 4}, false)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, results, 1)
|
||||
if len(results) > 1 {
|
||||
assert.Equal(t, 1, results[0].ID)
|
||||
}
|
||||
|
||||
results, err = user_model.GetMaileableUsersByIDs(db.DefaultContext, []int64{1, 4}, true)
|
||||
results, err = user_model.GetMailableUsersByIDs(db.DefaultContext, []int64{1, 4}, true)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, results, 2)
|
||||
if len(results) > 2 {
|
||||
|
||||
+27
-174
@@ -167,186 +167,39 @@ func (w *Webhook) UpdateEvent() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// HasCreateEvent returns true if hook enabled create event.
|
||||
func (w *Webhook) HasCreateEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Create)
|
||||
}
|
||||
|
||||
// HasDeleteEvent returns true if hook enabled delete event.
|
||||
func (w *Webhook) HasDeleteEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Delete)
|
||||
}
|
||||
|
||||
// HasForkEvent returns true if hook enabled fork event.
|
||||
func (w *Webhook) HasForkEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Fork)
|
||||
}
|
||||
|
||||
// HasIssuesEvent returns true if hook enabled issues event.
|
||||
func (w *Webhook) HasIssuesEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Issues)
|
||||
}
|
||||
|
||||
// HasIssuesAssignEvent returns true if hook enabled issues assign event.
|
||||
func (w *Webhook) HasIssuesAssignEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.IssueAssign)
|
||||
}
|
||||
|
||||
// HasIssuesLabelEvent returns true if hook enabled issues label event.
|
||||
func (w *Webhook) HasIssuesLabelEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.IssueLabel)
|
||||
}
|
||||
|
||||
// HasIssuesMilestoneEvent returns true if hook enabled issues milestone event.
|
||||
func (w *Webhook) HasIssuesMilestoneEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.IssueMilestone)
|
||||
}
|
||||
|
||||
// HasIssueCommentEvent returns true if hook enabled issue_comment event.
|
||||
func (w *Webhook) HasIssueCommentEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.IssueComment)
|
||||
}
|
||||
|
||||
// HasPushEvent returns true if hook enabled push event.
|
||||
func (w *Webhook) HasPushEvent() bool {
|
||||
return w.PushOnly || w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Push)
|
||||
}
|
||||
|
||||
// HasPullRequestEvent returns true if hook enabled pull request event.
|
||||
func (w *Webhook) HasPullRequestEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequest)
|
||||
}
|
||||
|
||||
// HasPullRequestAssignEvent returns true if hook enabled pull request assign event.
|
||||
func (w *Webhook) HasPullRequestAssignEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestAssign)
|
||||
}
|
||||
|
||||
// HasPullRequestLabelEvent returns true if hook enabled pull request label event.
|
||||
func (w *Webhook) HasPullRequestLabelEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestLabel)
|
||||
}
|
||||
|
||||
// HasPullRequestMilestoneEvent returns true if hook enabled pull request milestone event.
|
||||
func (w *Webhook) HasPullRequestMilestoneEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestMilestone)
|
||||
}
|
||||
|
||||
// HasPullRequestCommentEvent returns true if hook enabled pull_request_comment event.
|
||||
func (w *Webhook) HasPullRequestCommentEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestComment)
|
||||
}
|
||||
|
||||
// HasPullRequestApprovedEvent returns true if hook enabled pull request review event.
|
||||
func (w *Webhook) HasPullRequestApprovedEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestReview)
|
||||
}
|
||||
|
||||
// HasPullRequestRejectedEvent returns true if hook enabled pull request review event.
|
||||
func (w *Webhook) HasPullRequestRejectedEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestReview)
|
||||
}
|
||||
|
||||
// HasPullRequestReviewCommentEvent returns true if hook enabled pull request review event.
|
||||
func (w *Webhook) HasPullRequestReviewCommentEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestReview)
|
||||
}
|
||||
|
||||
// HasPullRequestSyncEvent returns true if hook enabled pull request sync event.
|
||||
func (w *Webhook) HasPullRequestSyncEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestSync)
|
||||
}
|
||||
|
||||
// HasWikiEvent returns true if hook enabled wiki event.
|
||||
func (w *Webhook) HasWikiEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvent.Wiki)
|
||||
}
|
||||
|
||||
// HasReleaseEvent returns if hook enabled release event.
|
||||
func (w *Webhook) HasReleaseEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Release)
|
||||
}
|
||||
|
||||
// HasRepositoryEvent returns if hook enabled repository event.
|
||||
func (w *Webhook) HasRepositoryEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Repository)
|
||||
}
|
||||
|
||||
// HasPackageEvent returns if hook enabled package event.
|
||||
func (w *Webhook) HasPackageEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Package)
|
||||
}
|
||||
|
||||
// HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event.
|
||||
func (w *Webhook) HasPullRequestReviewRequestEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestReviewRequest)
|
||||
}
|
||||
|
||||
// EventCheckers returns event checkers
|
||||
func (w *Webhook) EventCheckers() []struct {
|
||||
Has func() bool
|
||||
Type webhook_module.HookEventType
|
||||
} {
|
||||
return []struct {
|
||||
Has func() bool
|
||||
Type webhook_module.HookEventType
|
||||
}{
|
||||
{w.HasCreateEvent, webhook_module.HookEventCreate},
|
||||
{w.HasDeleteEvent, webhook_module.HookEventDelete},
|
||||
{w.HasForkEvent, webhook_module.HookEventFork},
|
||||
{w.HasPushEvent, webhook_module.HookEventPush},
|
||||
{w.HasIssuesEvent, webhook_module.HookEventIssues},
|
||||
{w.HasIssuesAssignEvent, webhook_module.HookEventIssueAssign},
|
||||
{w.HasIssuesLabelEvent, webhook_module.HookEventIssueLabel},
|
||||
{w.HasIssuesMilestoneEvent, webhook_module.HookEventIssueMilestone},
|
||||
{w.HasIssueCommentEvent, webhook_module.HookEventIssueComment},
|
||||
{w.HasPullRequestEvent, webhook_module.HookEventPullRequest},
|
||||
{w.HasPullRequestAssignEvent, webhook_module.HookEventPullRequestAssign},
|
||||
{w.HasPullRequestLabelEvent, webhook_module.HookEventPullRequestLabel},
|
||||
{w.HasPullRequestMilestoneEvent, webhook_module.HookEventPullRequestMilestone},
|
||||
{w.HasPullRequestCommentEvent, webhook_module.HookEventPullRequestComment},
|
||||
{w.HasPullRequestApprovedEvent, webhook_module.HookEventPullRequestReviewApproved},
|
||||
{w.HasPullRequestRejectedEvent, webhook_module.HookEventPullRequestReviewRejected},
|
||||
{w.HasPullRequestCommentEvent, webhook_module.HookEventPullRequestReviewComment},
|
||||
{w.HasPullRequestSyncEvent, webhook_module.HookEventPullRequestSync},
|
||||
{w.HasWikiEvent, webhook_module.HookEventWiki},
|
||||
{w.HasRepositoryEvent, webhook_module.HookEventRepository},
|
||||
{w.HasReleaseEvent, webhook_module.HookEventRelease},
|
||||
{w.HasPackageEvent, webhook_module.HookEventPackage},
|
||||
{w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest},
|
||||
func (w *Webhook) HasEvent(evt webhook_module.HookEventType) bool {
|
||||
if w.SendEverything {
|
||||
return true
|
||||
}
|
||||
if w.PushOnly {
|
||||
return evt == webhook_module.HookEventPush
|
||||
}
|
||||
checkEvt := evt
|
||||
switch evt {
|
||||
case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment:
|
||||
checkEvt = webhook_module.HookEventPullRequestReview
|
||||
}
|
||||
return w.HookEvents[checkEvt]
|
||||
}
|
||||
|
||||
// EventsArray returns an array of hook events
|
||||
func (w *Webhook) EventsArray() []string {
|
||||
events := make([]string, 0, 7)
|
||||
if w.SendEverything {
|
||||
events := make([]string, 0, len(webhook_module.AllEvents()))
|
||||
for _, evt := range webhook_module.AllEvents() {
|
||||
events = append(events, string(evt))
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
for _, c := range w.EventCheckers() {
|
||||
if c.Has() {
|
||||
events = append(events, string(c.Type))
|
||||
if w.PushOnly {
|
||||
return []string{string(webhook_module.HookEventPush)}
|
||||
}
|
||||
|
||||
events := make([]string, 0, len(w.HookEvents))
|
||||
for event, enabled := range w.HookEvents {
|
||||
if enabled {
|
||||
events = append(events, string(event))
|
||||
}
|
||||
}
|
||||
return events
|
||||
|
||||
@@ -11,6 +11,19 @@ import (
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
)
|
||||
|
||||
// GetSystemOrDefaultWebhooks returns webhooks by given argument or all if argument is missing.
|
||||
func GetSystemOrDefaultWebhooks(ctx context.Context, isSystemWebhook optional.Option[bool]) ([]*Webhook, error) {
|
||||
webhooks := make([]*Webhook, 0, 5)
|
||||
if !isSystemWebhook.Has() {
|
||||
return webhooks, db.GetEngine(ctx).Where("repo_id=? AND owner_id=?", 0, 0).
|
||||
Find(&webhooks)
|
||||
}
|
||||
|
||||
return webhooks, db.GetEngine(ctx).
|
||||
Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, isSystemWebhook.Value()).
|
||||
Find(&webhooks)
|
||||
}
|
||||
|
||||
// GetDefaultWebhooks returns all admin-default webhooks.
|
||||
func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) {
|
||||
webhooks := make([]*Webhook, 0, 5)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright 2017 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetSystemOrDefaultWebhooks(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
hooks, err := GetSystemOrDefaultWebhooks(db.DefaultContext, optional.None[bool]())
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, hooks, 2) {
|
||||
assert.Equal(t, int64(5), hooks[0].ID)
|
||||
assert.Equal(t, int64(6), hooks[1].ID)
|
||||
}
|
||||
|
||||
hooks, err = GetSystemOrDefaultWebhooks(db.DefaultContext, optional.Some(true))
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, hooks, 1) {
|
||||
assert.Equal(t, int64(5), hooks[0].ID)
|
||||
}
|
||||
|
||||
hooks, err = GetSystemOrDefaultWebhooks(db.DefaultContext, optional.Some(false))
|
||||
assert.NoError(t, err)
|
||||
if assert.Len(t, hooks, 1) {
|
||||
assert.Equal(t, int64(6), hooks[0].ID)
|
||||
}
|
||||
}
|
||||
@@ -54,9 +54,9 @@ func TestWebhook_UpdateEvent(t *testing.T) {
|
||||
SendEverything: false,
|
||||
ChooseEvents: false,
|
||||
HookEvents: webhook_module.HookEvents{
|
||||
Create: false,
|
||||
Push: true,
|
||||
PullRequest: false,
|
||||
webhook_module.HookEventCreate: false,
|
||||
webhook_module.HookEventPush: true,
|
||||
webhook_module.HookEventPullRequest: false,
|
||||
},
|
||||
}
|
||||
webhook.HookEvent = hookEvent
|
||||
@@ -68,13 +68,13 @@ func TestWebhook_UpdateEvent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWebhook_EventsArray(t *testing.T) {
|
||||
assert.Equal(t, []string{
|
||||
assert.EqualValues(t, []string{
|
||||
"create", "delete", "fork", "push",
|
||||
"issues", "issue_assign", "issue_label", "issue_milestone", "issue_comment",
|
||||
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
|
||||
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
|
||||
"pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release",
|
||||
"package", "pull_request_review_request",
|
||||
"pull_request_review_comment", "pull_request_sync", "pull_request_review_request", "wiki", "repository", "release",
|
||||
"package", "status",
|
||||
},
|
||||
(&Webhook{
|
||||
HookEvent: &webhook_module.HookEvent{SendEverything: true},
|
||||
|
||||
Reference in new issue
Block a user