aboutsummaryrefslogtreecommitdiff
path: root/models
diff options
context:
space:
mode:
authorAnthony Wang2023-02-20 22:21:24 +0000
committerAnthony Wang2023-02-20 22:21:24 +0000
commitdc20c2832871f6462990751ea802e14b02bf41b0 (patch)
tree71bfef0694e6c0f8284438c290521d7297f24eab /models
parent07df0a6b1c97be4b03d23d5dfa047a108de36592 (diff)
parentef11d41639dd1e89676e395068ee453312560adb (diff)
Merge remote-tracking branch 'origin/main' into forgejo-federation
Diffstat (limited to 'models')
-rw-r--r--models/activities/action.go2
-rw-r--r--models/activities/repo_activity.go6
-rw-r--r--models/asymkey/gpg_key_commit_verification.go11
-rw-r--r--models/asymkey/gpg_key_import.go2
-rw-r--r--models/asymkey/main_test.go2
-rw-r--r--models/avatars/avatar.go16
-rw-r--r--models/avatars/avatar_test.go11
-rw-r--r--models/db/context.go26
-rw-r--r--models/db/iterate_test.go2
-rw-r--r--models/db/sql_postgres_with_schema.go7
-rw-r--r--models/dbfs/main_test.go2
-rw-r--r--models/fixtures/issue.yml17
-rw-r--r--models/fixtures/label.yml45
-rw-r--r--models/fixtures/repo_unit.yml13
-rw-r--r--models/fixtures/repository.yml12
-rw-r--r--models/fixtures/user.yml134
-rw-r--r--models/git/commit_status.go3
-rw-r--r--models/git/protected_branch.go4
-rw-r--r--models/issues/comment.go9
-rw-r--r--models/issues/issue.go36
-rw-r--r--models/issues/issue_test.go19
-rw-r--r--models/issues/label.go106
-rw-r--r--models/issues/label_test.go45
-rw-r--r--models/issues/main_test.go2
-rw-r--r--models/issues/pull.go2
-rw-r--r--models/main_test.go2
-rw-r--r--models/migrations/base/tests.go6
-rw-r--r--models/migrations/fixtures/Test_DeleteOrphanedIssueLabels/label.yml5
-rw-r--r--models/migrations/migrations.go8
-rw-r--r--models/migrations/v1_19/v241.go17
-rw-r--r--models/migrations/v1_19/v242.go26
-rw-r--r--models/migrations/v1_19/v243.go16
-rw-r--r--models/organization/org.go4
-rw-r--r--models/organization/team.go10
-rw-r--r--models/organization/team_list.go2
-rw-r--r--models/perm/access/access.go8
-rw-r--r--models/perm/access/access_test.go4
-rw-r--r--models/perm/access/repo_permission.go8
-rw-r--r--models/project/board.go21
-rw-r--r--models/project/project.go34
-rw-r--r--models/project/project_test.go1
-rw-r--r--models/repo/attachment.go15
-rw-r--r--models/repo/avatar.go7
-rw-r--r--models/repo/repo.go10
-rw-r--r--models/repo/repo_unit.go1
-rw-r--r--models/repo/update.go2
-rw-r--r--models/repo/user_repo.go4
-rw-r--r--models/repo_collaboration_test.go2
-rw-r--r--models/system/setting.go59
-rw-r--r--models/system/setting_test.go13
-rw-r--r--models/unittest/fixtures.go10
-rw-r--r--models/user.go189
-rw-r--r--models/user/avatar.go16
-rw-r--r--models/user/must_change_password.go49
-rw-r--r--models/user/user.go105
-rw-r--r--models/user/user_test.go58
56 files changed, 734 insertions, 512 deletions
diff --git a/models/activities/action.go b/models/activities/action.go
index 8e7492c00..2e845bf89 100644
--- a/models/activities/action.go
+++ b/models/activities/action.go
@@ -534,7 +534,7 @@ func NotifyWatchers(ctx context.Context, actions ...*Action) error {
repo = act.Repo
// check repo owner exist.
- if err := act.Repo.GetOwner(ctx); err != nil {
+ if err := act.Repo.LoadOwner(ctx); err != nil {
return fmt.Errorf("can't get repo owner: %w", err)
}
} else if act.Repo == nil {
diff --git a/models/activities/repo_activity.go b/models/activities/repo_activity.go
index 9018276c3..72b6be312 100644
--- a/models/activities/repo_activity.go
+++ b/models/activities/repo_activity.go
@@ -97,12 +97,12 @@ func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository
}
users := make(map[int64]*ActivityAuthorData)
var unknownUserID int64
- unknownUserAvatarLink := user_model.NewGhostUser().AvatarLink()
+ unknownUserAvatarLink := user_model.NewGhostUser().AvatarLink(ctx)
for _, v := range code.Authors {
if len(v.Email) == 0 {
continue
}
- u, err := user_model.GetUserByEmail(v.Email)
+ u, err := user_model.GetUserByEmail(ctx, v.Email)
if u == nil || user_model.IsErrUserNotExist(err) {
unknownUserID--
users[unknownUserID] = &ActivityAuthorData{
@@ -119,7 +119,7 @@ func GetActivityStatsTopAuthors(ctx context.Context, repo *repo_model.Repository
users[u.ID] = &ActivityAuthorData{
Name: u.DisplayName(),
Login: u.LowerName,
- AvatarLink: u.AvatarLink(),
+ AvatarLink: u.AvatarLink(ctx),
HomeLink: u.HomeLink(),
Commits: v.Commits,
}
diff --git a/models/asymkey/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go
index 1b88fb8b1..db6e78cad 100644
--- a/models/asymkey/gpg_key_commit_verification.go
+++ b/models/asymkey/gpg_key_commit_verification.go
@@ -4,6 +4,7 @@
package asymkey
import (
+ "context"
"fmt"
"hash"
"strings"
@@ -70,14 +71,14 @@ const (
)
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
-func ParseCommitsWithSignature(oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error)) []*SignCommit {
+func ParseCommitsWithSignature(ctx context.Context, oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType, isOwnerMemberCollaborator func(*user_model.User) (bool, error)) []*SignCommit {
newCommits := make([]*SignCommit, 0, len(oldCommits))
keyMap := map[string]bool{}
for _, c := range oldCommits {
signCommit := &SignCommit{
UserCommit: c,
- Verification: ParseCommitWithSignature(c.Commit),
+ Verification: ParseCommitWithSignature(ctx, c.Commit),
}
_ = CalculateTrustStatus(signCommit.Verification, repoTrustModel, isOwnerMemberCollaborator, &keyMap)
@@ -88,13 +89,13 @@ func ParseCommitsWithSignature(oldCommits []*user_model.UserCommit, repoTrustMod
}
// ParseCommitWithSignature check if signature is good against keystore.
-func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
+func ParseCommitWithSignature(ctx context.Context, c *git.Commit) *CommitVerification {
var committer *user_model.User
if c.Committer != nil {
var err error
// Find Committer account
- committer, err = user_model.GetUserByEmail(c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not
- if err != nil { // Skipping not user for committer
+ committer, err = user_model.GetUserByEmail(ctx, c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not
+ if err != nil { // Skipping not user for committer
committer = &user_model.User{
Name: c.Committer.Name,
Email: c.Committer.Email,
diff --git a/models/asymkey/gpg_key_import.go b/models/asymkey/gpg_key_import.go
index 5b5ef4fab..83881b5c2 100644
--- a/models/asymkey/gpg_key_import.go
+++ b/models/asymkey/gpg_key_import.go
@@ -23,7 +23,7 @@ import "code.gitea.io/gitea/models/db"
// GPGKeyImport the original import of key
type GPGKeyImport struct {
KeyID string `xorm:"pk CHAR(16) NOT NULL"`
- Content string `xorm:"TEXT NOT NULL"`
+ Content string `xorm:"MEDIUMTEXT NOT NULL"`
}
func init() {
diff --git a/models/asymkey/main_test.go b/models/asymkey/main_test.go
index 82a408ece..7f8657189 100644
--- a/models/asymkey/main_test.go
+++ b/models/asymkey/main_test.go
@@ -13,7 +13,7 @@ import (
func init() {
setting.SetCustomPathAndConf("", "", "")
- setting.LoadForTest()
+ setting.InitProviderAndLoadCommonSettingsForTest()
}
func TestMain(m *testing.M) {
diff --git a/models/avatars/avatar.go b/models/avatars/avatar.go
index 438614286..6cf05dd28 100644
--- a/models/avatars/avatar.go
+++ b/models/avatars/avatar.go
@@ -21,7 +21,7 @@ import (
const (
// DefaultAvatarClass is the default class of a rendered avatar
- DefaultAvatarClass = "ui avatar vm"
+ DefaultAvatarClass = "ui avatar gt-vm"
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
DefaultAvatarPixelSize = 28
)
@@ -147,13 +147,13 @@ func generateRecognizedAvatarURL(u url.URL, size int) string {
// generateEmailAvatarLink returns a email avatar link.
// if final is true, it may use a slow path (eg: query DNS).
// if final is false, it always uses a fast path.
-func generateEmailAvatarLink(email string, size int, final bool) string {
+func generateEmailAvatarLink(ctx context.Context, email string, size int, final bool) string {
email = strings.TrimSpace(email)
if email == "" {
return DefaultAvatarLink()
}
- enableFederatedAvatar := system_model.GetSettingBool(system_model.KeyPictureEnableFederatedAvatar)
+ enableFederatedAvatar := system_model.GetSettingBool(ctx, system_model.KeyPictureEnableFederatedAvatar)
var err error
if enableFederatedAvatar && system_model.LibravatarService != nil {
@@ -174,7 +174,7 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
return urlStr
}
- disableGravatar := system_model.GetSettingBool(system_model.KeyPictureDisableGravatar)
+ disableGravatar := system_model.GetSettingBool(ctx, system_model.KeyPictureDisableGravatar)
if !disableGravatar {
// copy GravatarSourceURL, because we will modify its Path.
avatarURLCopy := *system_model.GravatarSourceURL
@@ -186,11 +186,11 @@ func generateEmailAvatarLink(email string, size int, final bool) string {
}
// GenerateEmailAvatarFastLink returns a avatar link (fast, the link may be a delegated one: "/avatar/${hash}")
-func GenerateEmailAvatarFastLink(email string, size int) string {
- return generateEmailAvatarLink(email, size, false)
+func GenerateEmailAvatarFastLink(ctx context.Context, email string, size int) string {
+ return generateEmailAvatarLink(ctx, email, size, false)
}
// GenerateEmailAvatarFinalLink returns a avatar final link (maybe slow)
-func GenerateEmailAvatarFinalLink(email string, size int) string {
- return generateEmailAvatarLink(email, size, true)
+func GenerateEmailAvatarFinalLink(ctx context.Context, email string, size int) string {
+ return generateEmailAvatarLink(ctx, email, size, true)
}
diff --git a/models/avatars/avatar_test.go b/models/avatars/avatar_test.go
index 29be2ea34..a3cb36d0e 100644
--- a/models/avatars/avatar_test.go
+++ b/models/avatars/avatar_test.go
@@ -7,6 +7,7 @@ import (
"testing"
avatars_model "code.gitea.io/gitea/models/avatars"
+ "code.gitea.io/gitea/models/db"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/modules/setting"
@@ -16,15 +17,15 @@ import (
const gravatarSource = "https://secure.gravatar.com/avatar/"
func disableGravatar(t *testing.T) {
- err := system_model.SetSettingNoVersion(system_model.KeyPictureEnableFederatedAvatar, "false")
+ err := system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureEnableFederatedAvatar, "false")
assert.NoError(t, err)
- err = system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, "true")
+ err = system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureDisableGravatar, "true")
assert.NoError(t, err)
system_model.LibravatarService = nil
}
func enableGravatar(t *testing.T) {
- err := system_model.SetSettingNoVersion(system_model.KeyPictureDisableGravatar, "false")
+ err := system_model.SetSettingNoVersion(db.DefaultContext, system_model.KeyPictureDisableGravatar, "false")
assert.NoError(t, err)
setting.GravatarSource = gravatarSource
err = system_model.Init()
@@ -47,11 +48,11 @@ func TestSizedAvatarLink(t *testing.T) {
disableGravatar(t)
assert.Equal(t, "/testsuburl/assets/img/avatar_default.png",
- avatars_model.GenerateEmailAvatarFastLink("gitea@example.com", 100))
+ avatars_model.GenerateEmailAvatarFastLink(db.DefaultContext, "gitea@example.com", 100))
enableGravatar(t)
assert.Equal(t,
"https://secure.gravatar.com/avatar/353cbad9b58e69c96154ad99f92bedc7?d=identicon&s=100",
- avatars_model.GenerateEmailAvatarFastLink("gitea@example.com", 100),
+ avatars_model.GenerateEmailAvatarFastLink(db.DefaultContext, "gitea@example.com", 100),
)
}
diff --git a/models/db/context.go b/models/db/context.go
index 911dbd1c6..4b3f7f0ee 100644
--- a/models/db/context.go
+++ b/models/db/context.go
@@ -7,6 +7,7 @@ import (
"context"
"database/sql"
+ "xorm.io/builder"
"xorm.io/xorm"
"xorm.io/xorm/schemas"
)
@@ -183,6 +184,31 @@ func DeleteByBean(ctx context.Context, bean interface{}) (int64, error) {
return GetEngine(ctx).Delete(bean)
}
+// DeleteByID deletes the given bean with the given ID
+func DeleteByID(ctx context.Context, id int64, bean interface{}) (int64, error) {
+ return GetEngine(ctx).ID(id).NoAutoTime().Delete(bean)
+}
+
+// FindIDs finds the IDs for the given table name satisfying the given condition
+// By passing a different value than "id" for "idCol", you can query for foreign IDs, i.e. the repo IDs which satisfy the condition
+func FindIDs(ctx context.Context, tableName, idCol string, cond builder.Cond) ([]int64, error) {
+ ids := make([]int64, 0, 10)
+ if err := GetEngine(ctx).Table(tableName).
+ Cols(idCol).
+ Where(cond).
+ Find(&ids); err != nil {
+ return nil, err
+ }
+ return ids, nil
+}
+
+// DecrByIDs decreases the given column for entities of the "bean" type with one of the given ids by one
+// Timestamps of the entities won't be updated
+func DecrByIDs(ctx context.Context, ids []int64, decrCol string, bean interface{}) error {
+ _, err := GetEngine(ctx).Decr(decrCol).In("id", ids).NoAutoCondition().NoAutoTime().Update(bean)
+ return err
+}
+
// DeleteBeans deletes all given beans, beans should contain delete conditions.
func DeleteBeans(ctx context.Context, beans ...interface{}) (err error) {
e := GetEngine(ctx)
diff --git a/models/db/iterate_test.go b/models/db/iterate_test.go
index 63487afa4..a713fe0d8 100644
--- a/models/db/iterate_test.go
+++ b/models/db/iterate_test.go
@@ -25,7 +25,7 @@ func TestIterate(t *testing.T) {
return nil
})
assert.NoError(t, err)
- assert.EqualValues(t, 81, repoCnt)
+ assert.EqualValues(t, 83, repoCnt)
err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error {
reopUnit2 := repo_model.RepoUnit{ID: repoUnit.ID}
diff --git a/models/db/sql_postgres_with_schema.go b/models/db/sql_postgres_with_schema.go
index c2694b37b..ec63447f6 100644
--- a/models/db/sql_postgres_with_schema.go
+++ b/models/db/sql_postgres_with_schema.go
@@ -37,7 +37,9 @@ func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) {
}
schemaValue, _ := driver.String.ConvertValue(setting.Database.Schema)
- if execer, ok := conn.(driver.Execer); ok {
+ // golangci lint is incorrect here - there is no benefit to using driver.ExecerContext here
+ // and in any case pq does not implement it
+ if execer, ok := conn.(driver.Execer); ok { //nolint
_, err := execer.Exec(`SELECT set_config(
'search_path',
$1 || ',' || current_setting('search_path'),
@@ -61,7 +63,8 @@ func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) {
// driver.String.ConvertValue will never return err for string
- _, err = stmt.Exec([]driver.Value{schemaValue})
+ // golangci lint is incorrect here - there is no benefit to using stmt.ExecWithContext here
+ _, err = stmt.Exec([]driver.Value{schemaValue}) //nolint
if err != nil {
_ = conn.Close()
return nil, err
diff --git a/models/dbfs/main_test.go b/models/dbfs/main_test.go
index 7a820b2d8..9dce663ee 100644
--- a/models/dbfs/main_test.go
+++ b/models/dbfs/main_test.go
@@ -13,7 +13,7 @@ import (
func init() {
setting.SetCustomPathAndConf("", "", "")
- setting.LoadForTest()
+ setting.InitProviderAndLoadCommonSettingsForTest()
}
func TestMain(m *testing.M) {
diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml
index 4dea8add1..174345ff5 100644
--- a/models/fixtures/issue.yml
+++ b/models/fixtures/issue.yml
@@ -287,3 +287,20 @@
created_unix: 1602935696
updated_unix: 1602935696
is_locked: false
+
+-
+ id: 18
+ repo_id: 55
+ index: 1
+ poster_id: 2
+ original_author_id: 0
+ name: issue for scoped labels
+ content: content
+ milestone_id: 0
+ priority: 0
+ is_closed: false
+ is_pull: false
+ num_comments: 0
+ created_unix: 946684830
+ updated_unix: 978307200
+ is_locked: false
diff --git a/models/fixtures/label.yml b/models/fixtures/label.yml
index 57bf80445..ab4d5ef94 100644
--- a/models/fixtures/label.yml
+++ b/models/fixtures/label.yml
@@ -4,6 +4,7 @@
org_id: 0
name: label1
color: '#abcdef'
+ exclusive: false
num_issues: 2
num_closed_issues: 0
@@ -13,6 +14,7 @@
org_id: 0
name: label2
color: '#000000'
+ exclusive: false
num_issues: 1
num_closed_issues: 1
@@ -22,6 +24,7 @@
org_id: 3
name: orglabel3
color: '#abcdef'
+ exclusive: false
num_issues: 0
num_closed_issues: 0
@@ -31,6 +34,7 @@
org_id: 3
name: orglabel4
color: '#000000'
+ exclusive: false
num_issues: 1
num_closed_issues: 0
@@ -40,5 +44,46 @@
org_id: 0
name: pull-test-label
color: '#000000'
+ exclusive: false
+ num_issues: 0
+ num_closed_issues: 0
+
+-
+ id: 6
+ repo_id: 55
+ org_id: 0
+ name: unscoped_label
+ color: '#000000'
+ exclusive: false
+ num_issues: 0
+ num_closed_issues: 0
+
+-
+ id: 7
+ repo_id: 55
+ org_id: 0
+ name: scope/label1
+ color: '#000000'
+ exclusive: true
+ num_issues: 0
+ num_closed_issues: 0
+
+-
+ id: 8
+ repo_id: 55
+ org_id: 0
+ name: scope/label2
+ color: '#000000'
+ exclusive: true
+ num_issues: 0
+ num_closed_issues: 0
+
+-
+ id: 9
+ repo_id: 55
+ org_id: 0
+ name: scope/subscope/label2
+ color: '#000000'
+ exclusive: true
num_issues: 0
num_closed_issues: 0
diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml
index 8706717ad..503b8c9dd 100644
--- a/models/fixtures/repo_unit.yml
+++ b/models/fixtures/repo_unit.yml
@@ -556,3 +556,16 @@
repo_id: 54
type: 1
created_unix: 946684810
+
+-
+ id: 82
+ repo_id: 31
+ type: 1
+ created_unix: 946684810
+
+-
+ id: 83
+ repo_id: 31
+ type: 3
+ config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}"
+ created_unix: 946684810
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index ef3cfbbbe..58f9b919a 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -1622,3 +1622,15 @@
is_archived: false
is_private: true
status: 0
+
+-
+ id: 55
+ owner_id: 2
+ owner_name: user2
+ lower_name: scoped_label
+ name: scoped_label
+ is_empty: false
+ is_archived: false
+ is_private: true
+ num_issues: 1
+ status: 0
diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml
index 63a5e0f89..b1c7fc003 100644
--- a/models/fixtures/user.yml
+++ b/models/fixtures/user.yml
@@ -8,8 +8,8 @@
email: user1@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user1
@@ -45,8 +45,8 @@
email: user2@example.com
keep_email_private: true
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user2
@@ -66,7 +66,7 @@
num_followers: 2
num_following: 1
num_stars: 2
- num_repos: 10
+ num_repos: 11
num_teams: 0
num_members: 0
visibility: 0
@@ -82,8 +82,8 @@
email: user3@example.com
keep_email_private: false
email_notifications_preference: onmention
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user3
@@ -119,8 +119,8 @@
email: user4@example.com
keep_email_private: false
email_notifications_preference: onmention
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user4
@@ -156,8 +156,8 @@
email: user5@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user5
@@ -193,8 +193,8 @@
email: user6@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user6
@@ -230,8 +230,8 @@
email: user7@example.com
keep_email_private: false
email_notifications_preference: disabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user7
@@ -267,8 +267,8 @@
email: user8@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user8
@@ -304,8 +304,8 @@
email: user9@example.com
keep_email_private: false
email_notifications_preference: onmention
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user9
@@ -341,8 +341,8 @@
email: user10@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user10
@@ -378,8 +378,8 @@
email: user11@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user11
@@ -415,8 +415,8 @@
email: user12@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user12
@@ -452,8 +452,8 @@
email: user13@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user13
@@ -489,8 +489,8 @@
email: user14@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user14
@@ -526,8 +526,8 @@
email: user15@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user15
@@ -563,8 +563,8 @@
email: user16@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user16
@@ -600,8 +600,8 @@
email: user17@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user17
@@ -637,8 +637,8 @@
email: user18@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user18
@@ -674,8 +674,8 @@
email: user19@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user19
@@ -711,8 +711,8 @@
email: user20@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user20
@@ -748,8 +748,8 @@
email: user21@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user21
@@ -785,8 +785,8 @@
email: limited_org@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: limited_org
@@ -822,8 +822,8 @@
email: privated_org@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: privated_org
@@ -859,8 +859,8 @@
email: user24@example.com
keep_email_private: true
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user24
@@ -896,8 +896,8 @@
email: org25@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: org25
@@ -933,8 +933,8 @@
email: org26@example.com
keep_email_private: false
email_notifications_preference: onmention
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: org26
@@ -970,8 +970,8 @@
email: user27@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user27
@@ -1007,8 +1007,8 @@
email: user28@example.com
keep_email_private: true
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user28
@@ -1044,8 +1044,8 @@
email: user29@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user29
@@ -1081,8 +1081,8 @@
email: user30@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user30
@@ -1118,8 +1118,8 @@
email: user31@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user31
@@ -1155,8 +1155,8 @@
email: user32@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: 7d93daa0d1e6f2305cc8fa496847d61dc7320bb16262f9c55dd753480207234cdd96a93194e408341971742f4701772a025a
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:notpassword
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user32
@@ -1192,8 +1192,8 @@
email: user33@example.com
keep_email_private: false
email_notifications_preference: enabled
- passwd: a3d5fcd92bae586c2e3dbe72daea7a0d27833a8d0227aa1704f4bbd775c1f3b03535b76dd93b0d4d8d22a519dca47df1547b
- passwd_hash_algo: argon2
+ passwd: ZogKvWdyEx:password
+ passwd_hash_algo: dummy
must_change_password: false
login_source: 0
login_name: user33
diff --git a/models/git/commit_status.go b/models/git/commit_status.go
index 7c40b6d21..489507f71 100644
--- a/models/git/commit_status.go
+++ b/models/git/commit_status.go
@@ -351,7 +351,8 @@ func hashCommitStatusContext(context string) string {
func ConvertFromGitCommit(ctx context.Context, commits []*git.Commit, repo *repo_model.Repository) []*SignCommitWithStatuses {
return ParseCommitsWithStatus(ctx,
asymkey_model.ParseCommitsWithSignature(
- user_model.ValidateCommitsWithEmails(commits),
+ ctx,
+ user_model.ValidateCommitsWithEmails(ctx, commits),
repo.GetTrustModel(),
func(user *user_model.User) (bool, error) {
return repo_model.IsOwnerMemberCollaborator(repo, user.ID)
diff --git a/models/git/protected_branch.go b/models/git/protected_branch.go
index 355a7464c..eef7e3935 100644
--- a/models/git/protected_branch.go
+++ b/models/git/protected_branch.go
@@ -314,8 +314,8 @@ type WhitelistOptions struct {
// This function also performs check if whitelist user and team's IDs have been changed
// to avoid unnecessary whitelist delete and regenerate.
func UpdateProtectBranch(ctx context.Context, repo *repo_model.Repository, protectBranch *ProtectedBranch, opts WhitelistOptions) (err error) {
- if err = repo.GetOwner(ctx); err != nil {
- return fmt.Errorf("GetOwner: %v", err)
+ if err = repo.LoadOwner(ctx); err != nil {
+ return fmt.Errorf("LoadOwner: %v", err)
}
whitelist, err := updateUserWhitelist(ctx, repo, protectBranch.WhitelistUserIDs, opts.UserIDs)
diff --git a/models/issues/comment.go b/models/issues/comment.go
index 2eefaffd8..bba270d4c 100644
--- a/models/issues/comment.go
+++ b/models/issues/comment.go
@@ -622,7 +622,7 @@ func (c *Comment) LoadAssigneeUserAndTeam() error {
return err
}
- if err = c.Issue.Repo.GetOwner(db.DefaultContext); err != nil {
+ if err = c.Issue.Repo.LoadOwner(db.DefaultContext); err != nil {
return err
}
@@ -826,7 +826,7 @@ func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment,
return nil, err
}
- if err = opts.Repo.GetOwner(ctx); err != nil {
+ if err = opts.Repo.LoadOwner(ctx); err != nil {
return nil, err
}
@@ -1247,6 +1247,11 @@ func FixCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) {
return res.RowsAffected()
}
+// HasOriginalAuthor returns if a comment was migrated and has an original author.
+func (c *Comment) HasOriginalAuthor() bool {
+ return c.OriginalAuthor != "" && c.OriginalAuthorID != 0
+}
+
func (c *Comment) GetIRI(ctx context.Context) string {
err := c.LoadIssue(ctx)
if err != nil {
diff --git a/models/issues/issue.go b/models/issues/issue.go
index 63e153d7b..99ec1437f 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -540,6 +540,31 @@ func (ts labelSorter) Swap(i, j int) {
[]*Label(ts)[i], []*Label(ts)[j] = []*Label(ts)[j], []*Label(ts)[i]
}
+// Ensure only one label of a given scope exists, with labels at the end of the
+// array getting preference over earlier ones.
+func RemoveDuplicateExclusiveLabels(labels []*Label) []*Label {
+ validLabels := make([]*Label, 0, len(labels))
+
+ for i, label := range labels {
+ scope := label.ExclusiveScope()
+ if scope != "" {
+ foundOther := false
+ for _, otherLabel := range labels[i+1:] {
+ if otherLabel.ExclusiveScope() == scope {
+ foundOther = true
+ break
+ }
+ }
+ if foundOther {
+ continue
+ }
+ }
+ validLabels = append(validLabels, label)
+ }
+
+ return validLabels
+}
+
// ReplaceIssueLabels removes all current labels and add new labels to the issue.
// Triggers appropriate WebHooks, if any.
func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (err error) {
@@ -557,6 +582,8 @@ func ReplaceIssueLabels(issue *Issue, labels []*Label, doer *user_model.User) (e
return err
}
+ labels = RemoveDuplicateExclusiveLabels(labels)
+
sort.Sort(labelSorter(labels))
sort.Sort(labelSorter(issue.Labels))
@@ -2101,7 +2128,7 @@ func ResolveIssueMentionsByVisibility(ctx context.Context, issue *Issue, doer *u
resolved := make(map[string]bool, 10)
var mentionTeams []string
- if err := issue.Repo.GetOwner(ctx); err != nil {
+ if err := issue.Repo.LoadOwner(ctx); err != nil {
return nil, err
}
@@ -2406,6 +2433,11 @@ func DeleteOrphanedIssues(ctx context.Context) error {
return nil
}
+// HasOriginalAuthor returns if an issue was migrated and has an original author.
+func (issue *Issue) HasOriginalAuthor() bool {
+ return issue.OriginalAuthor != "" && issue.OriginalAuthorID != 0
+}
+
func (issue *Issue) GetIRI(ctx context.Context) string {
err := issue.LoadRepo(ctx)
if err != nil {
@@ -2415,4 +2447,4 @@ func (issue *Issue) GetIRI(ctx context.Context) string {
return issue.OriginalAuthor
}
return setting.AppURL + "api/v1/activitypub/ticket/" + issue.Repo.OwnerName + "/" + issue.Repo.Name + "/" + strconv.FormatInt(issue.Index, 10)
-}
+} \ No newline at end of file
diff --git a/models/issues/issue_test.go b/models/issues/issue_test.go
index de1da19ab..3a83d8d2b 100644
--- a/models/issues/issue_test.go
+++ b/models/issues/issue_test.go
@@ -25,7 +25,7 @@ import (
func TestIssue_ReplaceLabels(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
- testSuccess := func(issueID int64, labelIDs []int64) {
+ testSuccess := func(issueID int64, labelIDs, expectedLabelIDs []int64) {
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: issueID})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
@@ -35,15 +35,20 @@ func TestIssue_ReplaceLabels(t *testing.T) {
labels[i] = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: labelID, RepoID: repo.ID})
}
assert.NoError(t, issues_model.ReplaceIssueLabels(issue, labels, doer))
- unittest.AssertCount(t, &issues_model.IssueLabel{IssueID: issueID}, len(labelIDs))
- for _, labelID := range labelIDs {
+ unittest.AssertCount(t, &issues_model.IssueLabel{IssueID: issueID}, len(expectedLabelIDs))
+ for _, labelID := range expectedLabelIDs {
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: labelID})
}
}
- testSuccess(1, []int64{2})
- testSuccess(1, []int64{1, 2})
- testSuccess(1, []int64{})
+ testSuccess(1, []int64{2}, []int64{2})
+ testSuccess(1, []int64{1, 2}, []int64{1, 2})
+ testSuccess(1, []int64{}, []int64{})
+
+ // mutually exclusive scoped labels 7 and 8
+ testSuccess(18, []int64{6, 7}, []int64{6, 7})
+ testSuccess(18, []int64{7, 8}, []int64{8})
+ testSuccess(18, []int64{6, 8, 7}, []int64{6, 7})
}
func Test_GetIssueIDsByRepoID(t *testing.T) {
@@ -523,5 +528,5 @@ func TestCountIssues(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
count, err := issues_model.CountIssues(db.DefaultContext, &issues_model.IssuesOptions{})
assert.NoError(t, err)
- assert.EqualValues(t, 17, count)
+ assert.EqualValues(t, 18, count)
}
diff --git a/models/issues/label.go b/models/issues/label.go
index dbb7a139e..0dd12fb5c 100644
--- a/models/issues/label.go
+++ b/models/issues/label.go
@@ -7,8 +7,6 @@ package issues
import (
"context"
"fmt"
- "html/template"
- "math"
"regexp"
"strconv"
"strings"
@@ -89,6 +87,7 @@ type Label struct {
RepoID int64 `xorm:"INDEX"`
OrgID int64 `xorm:"INDEX"`
Name string
+ Exclusive bool
Description string
Color string `xorm:"VARCHAR(7)"`
NumIssues int
@@ -128,18 +127,22 @@ func (label *Label) CalOpenOrgIssues(ctx context.Context, repoID, labelID int64)
}
// LoadSelectedLabelsAfterClick calculates the set of selected labels when a label is clicked
-func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64) {
+func (label *Label) LoadSelectedLabelsAfterClick(currentSelectedLabels []int64, currentSelectedExclusiveScopes []string) {
var labelQuerySlice []string
labelSelected := false
labelID := strconv.FormatInt(label.ID, 10)
- for _, s := range currentSelectedLabels {
+ labelScope := label.ExclusiveScope()
+ for i, s := range currentSelectedLabels {
if s == label.ID {
labelSelected = true
} else if -s == label.ID {
labelSelected = true
label.IsExcluded = true
} else if s != 0 {
- labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10))
+ // Exclude other labels in the same scope from selection
+ if s < 0 || labelScope == "" || labelScope != currentSelectedExclusiveScopes[i] {
+ labelQuerySlice = append(labelQuerySlice, strconv.FormatInt(s, 10))
+ }
}
}
if !labelSelected {
@@ -159,49 +162,43 @@ func (label *Label) BelongsToRepo() bool {
return label.RepoID > 0
}
-// SrgbToLinear converts a component of an sRGB color to its linear intensity
-// See: https://en.wikipedia.org/wiki/SRGB#The_reverse_transformation_(sRGB_to_CIE_XYZ)
-func SrgbToLinear(color uint8) float64 {
- flt := float64(color) / 255
- if flt <= 0.04045 {
- return flt / 12.92
+// Get color as RGB values in 0..255 range
+func (label *Label) ColorRGB() (float64, float64, float64, error) {
+ color, err := strconv.ParseUint(label.Color[1:], 16, 64)
+ if err != nil {
+ return 0, 0, 0, err
}
- return math.Pow((flt+0.055)/1.055, 2.4)
-}
-
-// Luminance returns the luminance of an sRGB color
-func Luminance(color uint32) float64 {
- r := SrgbToLinear(uint8(0xFF & (color >> 16)))
- g := SrgbToLinear(uint8(0xFF & (color >> 8)))
- b := SrgbToLinear(uint8(0xFF & color))
- // luminance ratios for sRGB
- return 0.2126*r + 0.7152*g + 0.0722*b
+ r := float64(uint8(0xFF & (uint32(color) >> 16)))
+ g := float64(uint8(0xFF & (uint32(color) >> 8)))
+ b := float64(uint8(0xFF & uint32(color)))
+ return r, g, b, nil
}
-// LuminanceThreshold is the luminance at which white and black appear to have the same contrast
-// i.e. x such that 1.05 / (x + 0.05) = (x + 0.05) / 0.05
-// i.e. math.Sqrt(1.05*0.05) - 0.05
-const LuminanceThreshold float64 = 0.179
-
-// ForegroundColor calculates the text color for labels based
-// on their background color.
-func (label *Label) ForegroundColor() template.CSS {
+// Determine if label text should be light or dark to be readable on background color
+func (label *Label) UseLightTextColor() bool {
if strings.HasPrefix(label.Color, "#") {
- if color, err := strconv.ParseUint(label.Color[1:], 16, 64); err == nil {
- // NOTE: see web_src/js/components/ContextPopup.vue for similar implementation
- luminance := Luminance(uint32(color))
-
- // prefer white or black based upon contrast
- if luminance < LuminanceThreshold {
- return template.CSS("#fff")
- }
- return template.CSS("#000")
+ if r, g, b, err := label.ColorRGB(); err == nil {
+ // Perceived brightness from: https://www.w3.org/TR/AERT/#color-contrast
+ // In the future WCAG 3 APCA may be a better solution
+ brightness := (0.299*r + 0.587*g + 0.114*b) / 255
+ return brightness < 0.35
}
}
- // default to black
- return template.CSS("#000")
+ return false
+}
+
+// Return scope substring of label name, or empty string if none exists
+func (label *Label) ExclusiveScope() string {
+ if !label.Exclusive {
+ return ""
+ }
+ lastIndex := strings.LastIndex(label.Name, "/")
+ if lastIndex == -1 || lastIndex == 0 || lastIndex == len(label.Name)-1 {
+ return ""
+ }
+ return label.Name[:lastIndex]
}
// NewLabel creates a new label
@@ -253,7 +250,7 @@ func UpdateLabel(l *Label) error {
if !LabelColorPattern.MatchString(l.Color) {
return fmt.Errorf("bad color code: %s", l.Color)
}
- return updateLabelCols(db.DefaultContext, l, "name", "description", "color")
+ return updateLabelCols(db.DefaultContext, l, "name", "description", "color", "exclusive")
}
// DeleteLabel delete a label
@@ -620,6 +617,29 @@ func newIssueLabel(ctx context.Context, issue *Issue, label *Label, doer *user_m
return updateLabelCols(ctx, label, "num_issues", "num_closed_issue")
}
+// Remove all issue labels in the given exclusive scope
+func RemoveDuplicateExclusiveIssueLabels(ctx context.Context, issue *Issue, label *Label, doer *user_model.User) (err error) {
+ scope := label.ExclusiveScope()
+ if scope == "" {
+ return nil
+ }
+
+ var toRemove []*Label
+ for _, issueLabel := range issue.Labels {
+ if label.ID != issueLabel.ID && issueLabel.ExclusiveScope() == scope {
+ toRemove = append(toRemove, issueLabel)
+ }
+ }
+
+ for _, issueLabel := range toRemove {
+ if err = deleteIssueLabel(ctx, issue, issueLabel, doer); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
// NewIssueLabel creates a new issue-label relation.
func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error) {
if HasIssueLabel(db.DefaultContext, issue.ID, label.ID) {
@@ -641,6 +661,10 @@ func NewIssueLabel(issue *Issue, label *Label, doer *user_model.User) (err error
return nil
}
+ if err = RemoveDuplicateExclusiveIssueLabels(ctx, issue, label, doer); err != nil {
+ return nil
+ }
+
if err = newIssueLabel(ctx, issue, label, doer); err != nil {
return err
}
diff --git a/models/issues/label_test.go b/models/issues/label_test.go
index 239e328d4..0e45e0db0 100644
--- a/models/issues/label_test.go
+++ b/models/issues/label_test.go
@@ -4,7 +4,6 @@
package issues_test
import (
- "html/template"
"testing"
"code.gitea.io/gitea/models/db"
@@ -25,13 +24,22 @@ func TestLabel_CalOpenIssues(t *testing.T) {
assert.EqualValues(t, 2, label.NumOpenIssues)
}
-func TestLabel_ForegroundColor(t *testing.T) {
+func TestLabel_TextColor(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
- assert.Equal(t, template.CSS("#000"), label.ForegroundColor())
+ assert.False(t, label.UseLightTextColor())
label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 2})
- assert.Equal(t, template.CSS("#fff"), label.ForegroundColor())
+ assert.True(t, label.UseLightTextColor())
+}
+
+func TestLabel_ExclusiveScope(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ label := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 7})
+ assert.Equal(t, "scope", label.ExclusiveScope())
+
+ label = unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 9})
+ assert.Equal(t, "scope/subscope", label.ExclusiveScope())
}
func TestNewLabels(t *testing.T) {
@@ -266,6 +274,7 @@ func TestUpdateLabel(t *testing.T) {
Color: "#ffff00",
Name: "newLabelName",
Description: label.Description,
+ Exclusive: false,
}
label.Color = update.Color
label.Name = update.Name
@@ -323,6 +332,34 @@ func TestNewIssueLabel(t *testing.T) {
unittest.CheckConsistencyFor(t, &issues_model.Issue{}, &issues_model.Label{})
}
+func TestNewIssueExclusiveLabel(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 18})
+ doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ otherLabel := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 6})
+ exclusiveLabelA := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 7})
+ exclusiveLabelB := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 8})
+
+ // coexisting regular and exclusive label
+ assert.NoError(t, issues_model.NewIssueLabel(issue, otherLabel, doer))
+ assert.NoError(t, issues_model.NewIssueLabel(issue, exclusiveLabelA, doer))
+ unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: otherLabel.ID})
+ unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: exclusiveLabelA.ID})
+
+ // exclusive label replaces existing one
+ assert.NoError(t, issues_model.NewIssueLabel(issue, exclusiveLabelB, doer))
+ unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: otherLabel.ID})
+ unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: exclusiveLabelB.ID})
+ unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: exclusiveLabelA.ID})
+
+ // exclusive label replaces existing one again
+ assert.NoError(t, issues_model.NewIssueLabel(issue, exclusiveLabelA, doer))
+ unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: otherLabel.ID})
+ unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: exclusiveLabelA.ID})
+ unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: issue.ID, LabelID: exclusiveLabelB.ID})
+}
+
func TestNewIssueLabels(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
label1 := unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ID: 1})
diff --git a/models/issues/main_test.go b/models/issues/main_test.go
index 93e05f33f..de84da30e 100644
--- a/models/issues/main_test.go
+++ b/models/issues/main_test.go
@@ -20,7 +20,7 @@ import (
func init() {
setting.SetCustomPathAndConf("", "", "")
- setting.LoadForTest()
+ setting.InitProviderAndLoadCommonSettingsForTest()
}
func TestFixturesAreConsistent(t *testing.T) {
diff --git a/models/issues/pull.go b/models/issues/pull.go
index 3f8b0bc7a..6a1dc3155 100644
--- a/models/issues/pull.go
+++ b/models/issues/pull.go
@@ -498,7 +498,7 @@ func (pr *PullRequest) SetMerged(ctx context.Context) (bool, error) {
return false, err
}
- if err := pr.Issue.Repo.GetOwner(ctx); err != nil {
+ if err := pr.Issue.Repo.LoadOwner(ctx); err != nil {
return false, err
}
diff --git a/models/main_test.go b/models/main_test.go
index cc4eebfe7..b5919bb28 100644
--- a/models/main_test.go
+++ b/models/main_test.go
@@ -20,7 +20,7 @@ import (
func init() {
setting.SetCustomPathAndConf("", "", "")
- setting.LoadForTest()
+ setting.InitProviderAndLoadCommonSettingsForTest()
}
// TestFixturesAreConsistent assert that test fixtures are consistent
diff --git a/models/migrations/base/tests.go b/models/migrations/base/tests.go
index a9bcd20f6..2f1b24664 100644
--- a/models/migrations/base/tests.go
+++ b/models/migrations/base/tests.go
@@ -149,13 +149,13 @@ func MainTest(m *testing.M) {
setting.AppDataPath = tmpDataPath
setting.SetCustomPathAndConf("", "", "")
- setting.LoadForTest()
+ setting.InitProviderAndLoadCommonSettingsForTest()
if err = git.InitFull(context.Background()); err != nil {
fmt.Printf("Unable to InitFull: %v\n", err)
os.Exit(1)
}
- setting.InitDBConfig()
- setting.NewLogServices(true)
+ setting.LoadDBSetting()
+ setting.InitLogs(true)
exitStatus := m.Run()
diff --git a/models/migrations/fixtures/Test_DeleteOrphanedIssueLabels/label.yml b/models/migrations/fixtures/Test_DeleteOrphanedIssueLabels/label.yml
index d651c87d5..085b7f088 100644
--- a/models/migrations/fixtures/Test_DeleteOrphanedIssueLabels/label.yml
+++ b/models/migrations/fixtures/Test_DeleteOrphanedIssueLabels/label.yml
@@ -4,6 +4,7 @@
org_id: 0
name: label1
color: '#abcdef'
+ exclusive: false
num_issues: 2
num_closed_issues: 0
@@ -13,6 +14,7 @@
org_id: 0
name: label2
color: '#000000'
+ exclusive: false
num_issues: 1
num_closed_issues: 1
-
@@ -21,6 +23,7 @@
org_id: 3
name: orglabel3
color: '#abcdef'
+ exclusive: false
num_issues: 0
num_closed_issues: 0
@@ -30,6 +33,7 @@
org_id: 3
name: orglabel4
color: '#000000'
+ exclusive: false
num_issues: 1
num_closed_issues: 0
@@ -39,5 +43,6 @@
org_id: 0
name: pull-test-label
color: '#000000'
+ exclusive: false
num_issues: 0
num_closed_issues: 0
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 15600f057..989a1d6ae 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -455,6 +455,14 @@ var migrations = []Migration{
NewMigration("Add scope for access_token", v1_19.AddScopeForAccessTokens),
// v240 -> v241
NewMigration("Add actions tables", v1_19.AddActionsTables),
+ // v241 -> v242
+ NewMigration("Add card_type column to project table", v1_19.AddCardTypeToProjectTable),
+ // v242 -> v243
+ NewMigration("Alter gpg_key_import content TEXT field to MEDIUMTEXT", v1_19.AlterPublicGPGKeyImportContentFieldToMediumText),
+ // v243 -> v244
+ NewMigration("Add exclusive label", v1_19.AddExclusiveLabel),
+
+ // Gitea 1.19.0 ends at v244
}
// GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_19/v241.go b/models/migrations/v1_19/v241.go
new file mode 100644
index 000000000..a617d6fd2
--- /dev/null
+++ b/models/migrations/v1_19/v241.go
@@ -0,0 +1,17 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_19 //nolint
+
+import (
+ "xorm.io/xorm"
+)
+
+// AddCardTypeToProjectTable: add CardType column, setting existing rows to CardTypeTextOnly
+func AddCardTypeToProjectTable(x *xorm.Engine) error {
+ type Project struct {
+ CardType int `xorm:"NOT NULL DEFAULT 0"`
+ }
+
+ return x.Sync(new(Project))
+}
diff --git a/models/migrations/v1_19/v242.go b/models/migrations/v1_19/v242.go
new file mode 100644
index 000000000..517c7767b
--- /dev/null
+++ b/models/migrations/v1_19/v242.go
@@ -0,0 +1,26 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_19 //nolint
+
+import (
+ "code.gitea.io/gitea/modules/setting"
+
+ "xorm.io/xorm"
+)
+
+// AlterPublicGPGKeyImportContentFieldToMediumText: set GPGKeyImport Content field to MEDIUMTEXT
+func AlterPublicGPGKeyImportContentFieldToMediumText(x *xorm.Engine) error {
+ sess := x.NewSession()
+ defer sess.Close()
+ if err := sess.Begin(); err != nil {
+ return err
+ }
+
+ if setting.Database.UseMySQL {
+ if _, err := sess.Exec("ALTER TABLE `gpg_key_import` CHANGE `content` `content` MEDIUMTEXT"); err != nil {
+ return err
+ }
+ }
+ return sess.Commit()
+}
diff --git a/models/migrations/v1_19/v243.go b/models/migrations/v1_19/v243.go
new file mode 100644
index 000000000..55bbfafb2
--- /dev/null
+++ b/models/migrations/v1_19/v243.go
@@ -0,0 +1,16 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_19 //nolint
+
+import (
+ "xorm.io/xorm"
+)
+
+func AddExclusiveLabel(x *xorm.Engine) error {
+ type Label struct {
+ Exclusive bool
+ }
+
+ return x.Sync(new(Label))
+}
diff --git a/models/organization/org.go b/models/organization/org.go
index 852facf70..f05027be7 100644
--- a/models/organization/org.go
+++ b/models/organization/org.go
@@ -156,8 +156,8 @@ func (org *Organization) hasMemberWithUserID(ctx context.Context, userID int64)
}
// AvatarLink returns the full avatar link with http host
-func (org *Organization) AvatarLink() string {
- return org.AsUser().AvatarLink()
+func (org *Organization) AvatarLink(ctx context.Context) string {
+ return org.AsUser().AvatarLink(ctx)
}
// HTMLURL returns the organization's full link.
diff --git a/models/organization/team.go b/models/organization/team.go
index 0c2577dab..5e3c9ecff 100644
--- a/models/organization/team.go
+++ b/models/organization/team.go
@@ -111,12 +111,8 @@ func (t *Team) ColorFormat(s fmt.State) {
t.AccessMode)
}
-// GetUnits return a list of available units for a team
-func (t *Team) GetUnits() error {
- return t.getUnits(db.DefaultContext)
-}
-
-func (t *Team) getUnits(ctx context.Context) (err error) {
+// LoadUnits load a list of available units for a team
+func (t *Team) LoadUnits(ctx context.Context) (err error) {
if t.Units != nil {
return nil
}
@@ -193,7 +189,7 @@ func (t *Team) UnitEnabled(ctx context.Context, tp unit.Type) bool {
// UnitAccessMode returns if the team has the given unit type enabled
func (t *Team) UnitAccessMode(ctx context.Context, tp unit.Type) perm.AccessMode {
- if err := t.getUnits(ctx); err != nil {
+ if err := t.LoadUnits(ctx); err != nil {
log.Warn("Error loading team (ID: %d) units: %s", t.ID, err.Error())
}
diff --git a/models/organization/team_list.go b/models/organization/team_list.go
index 5d3bd555c..efb3104ad 100644
--- a/models/organization/team_list.go
+++ b/models/organization/team_list.go
@@ -19,7 +19,7 @@ type TeamList []*Team
func (t TeamList) LoadUnits(ctx context.Context) error {
for _, team := range t {
- if err := team.getUnits(ctx); err != nil {
+ if err := team.LoadUnits(ctx); err != nil {
return err
}
}
diff --git a/models/perm/access/access.go b/models/perm/access/access.go
index 48ecf78a8..2d1b2daa6 100644
--- a/models/perm/access/access.go
+++ b/models/perm/access/access.go
@@ -85,8 +85,8 @@ func updateUserAccess(accessMap map[int64]*userAccess, user *user_model.User, mo
// FIXME: do cross-comparison so reduce deletions and additions to the minimum?
func refreshAccesses(ctx context.Context, repo *repo_model.Repository, accessMap map[int64]*userAccess) (err error) {
minMode := perm.AccessModeRead
- if err := repo.GetOwner(ctx); err != nil {
- return fmt.Errorf("GetOwner: %w", err)
+ if err := repo.LoadOwner(ctx); err != nil {
+ return fmt.Errorf("LoadOwner: %w", err)
}
// If the repo isn't private and isn't owned by a organization,
@@ -143,7 +143,7 @@ func refreshCollaboratorAccesses(ctx context.Context, repoID int64, accessMap ma
func RecalculateTeamAccesses(ctx context.Context, repo *repo_model.Repository, ignTeamID int64) (err error) {
accessMap := make(map[int64]*userAccess, 20)
- if err = repo.GetOwner(ctx); err != nil {
+ if err = repo.LoadOwner(ctx); err != nil {
return err
} else if !repo.Owner.IsOrganization() {
return fmt.Errorf("owner is not an organization: %d", repo.OwnerID)
@@ -199,7 +199,7 @@ func RecalculateUserAccess(ctx context.Context, repo *repo_model.Repository, uid
accessMode = collaborator.Mode
}
- if err = repo.GetOwner(ctx); err != nil {
+ if err = repo.LoadOwner(ctx); err != nil {
return err
} else if repo.Owner.IsOrganization() {
var teams []organization.Team
diff --git a/models/perm/access/access_test.go b/models/perm/access/access_test.go
index bd828a1e9..79b131fe8 100644
--- a/models/perm/access/access_test.go
+++ b/models/perm/access/access_test.go
@@ -97,7 +97,7 @@ func TestRepository_RecalculateAccesses(t *testing.T) {
// test with organization repo
assert.NoError(t, unittest.PrepareTestDatabase())
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
- assert.NoError(t, repo1.GetOwner(db.DefaultContext))
+ assert.NoError(t, repo1.LoadOwner(db.DefaultContext))
_, err := db.GetEngine(db.DefaultContext).Delete(&repo_model.Collaboration{UserID: 2, RepoID: 3})
assert.NoError(t, err)
@@ -114,7 +114,7 @@ func TestRepository_RecalculateAccesses2(t *testing.T) {
// test with non-organization repo
assert.NoError(t, unittest.PrepareTestDatabase())
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
- assert.NoError(t, repo1.GetOwner(db.DefaultContext))
+ assert.NoError(t, repo1.LoadOwner(db.DefaultContext))
_, err := db.GetEngine(db.DefaultContext).Delete(&repo_model.Collaboration{UserID: 4, RepoID: 4})
assert.NoError(t, err)
diff --git a/models/perm/access/repo_permission.go b/models/perm/access/repo_permission.go
index a6bf9b674..ee76e4820 100644
--- a/models/perm/access/repo_permission.go
+++ b/models/perm/access/repo_permission.go
@@ -175,7 +175,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
}
}
- if err = repo.GetOwner(ctx); err != nil {
+ if err = repo.LoadOwner(ctx); err != nil {
return
}
@@ -210,7 +210,7 @@ func GetUserRepoPermission(ctx context.Context, repo *repo_model.Repository, use
return
}
- if err = repo.GetOwner(ctx); err != nil {
+ if err = repo.LoadOwner(ctx); err != nil {
return
}
if !repo.Owner.IsOrganization() {
@@ -281,7 +281,7 @@ func IsUserRealRepoAdmin(repo *repo_model.Repository, user *user_model.User) (bo
return true, nil
}
- if err := repo.GetOwner(db.DefaultContext); err != nil {
+ if err := repo.LoadOwner(db.DefaultContext); err != nil {
return false, err
}
@@ -378,7 +378,7 @@ func HasAccess(ctx context.Context, userID int64, repo *repo_model.Repository) (
// getUsersWithAccessMode returns users that have at least given access mode to the repository.
func getUsersWithAccessMode(ctx context.Context, repo *repo_model.Repository, mode perm_model.AccessMode) (_ []*user_model.User, err error) {
- if err = repo.GetOwner(ctx); err != nil {
+ if err = repo.LoadOwner(ctx); err != nil {
return nil, err
}
diff --git a/models/project/board.go b/models/project/board.go
index d8468f0cb..dc4e2e688 100644
--- a/models/project/board.go
+++ b/models/project/board.go
@@ -19,6 +19,9 @@ type (
// BoardType is used to represent a project board type
BoardType uint8
+ // CardType is used to represent a project board card type
+ CardType uint8
+
// BoardList is a list of all project boards in a repository
BoardList []*Board
)
@@ -34,6 +37,14 @@ const (
BoardTypeBugTriage
)
+const (
+ // CardTypeTextOnly is a project board card type that is text only
+ CardTypeTextOnly CardType = iota
+
+ // CardTypeImagesAndText is a project board card type that has images and text
+ CardTypeImagesAndText
+)
+
// BoardColorPattern is a regexp witch can validate BoardColor
var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
@@ -85,6 +96,16 @@ func IsBoardTypeValid(p BoardType) bool {
}
}
+// IsCardTypeValid checks if the project board card type is valid
+func IsCardTypeValid(p CardType) bool {
+ switch p {
+ case CardTypeTextOnly, CardTypeImagesAndText:
+ return true
+ default:
+ return false
+ }
+}
+
func createBoardsForProjectsType(ctx context.Context, project *Project) error {
var items []string
diff --git a/models/project/project.go b/models/project/project.go
index 9074fd0c1..931ef4467 100644
--- a/models/project/project.go
+++ b/models/project/project.go
@@ -19,12 +19,18 @@ import (
)
type (
- // ProjectsConfig is used to identify the type of board that is being created
- ProjectsConfig struct {
+ // BoardConfig is used to identify the type of board that is being created
+ BoardConfig struct {
BoardType BoardType
Translation string
}
+ // CardConfig is used to identify the type of board card that is being used
+ CardConfig struct {
+ CardType CardType
+ Translation string
+ }
+
// Type is used to identify the type of project in question and ownership
Type uint8
)
@@ -91,6 +97,7 @@ type Project struct {
CreatorID int64 `xorm:"NOT NULL"`
IsClosed bool `xorm:"INDEX"`
BoardType BoardType
+ CardType CardType
Type Type
RenderedContent string `xorm:"-"`
@@ -145,15 +152,23 @@ func init() {
db.RegisterModel(new(Project))
}
-// GetProjectsConfig retrieves the types of configurations projects could have
-func GetProjectsConfig() []ProjectsConfig {
- return []ProjectsConfig{
+// GetBoardConfig retrieves the types of configurations project boards could have
+func GetBoardConfig() []BoardConfig {
+ return []BoardConfig{
{BoardTypeNone, "repo.projects.type.none"},
{BoardTypeBasicKanban, "repo.projects.type.basic_kanban"},
{BoardTypeBugTriage, "repo.projects.type.bug_triage"},
}
}
+// GetCardConfig retrieves the types of configurations project board cards could have
+func GetCardConfig() []CardConfig {
+ return []CardConfig{
+ {CardTypeTextOnly, "repo.projects.card_type.text_only"},
+ {CardTypeImagesAndText, "repo.projects.card_type.images_and_text"},
+ }
+}
+
// IsTypeValid checks if a project type is valid
func IsTypeValid(p Type) bool {
switch p {
@@ -237,6 +252,10 @@ func NewProject(p *Project) error {
p.BoardType = BoardTypeNone
}
+ if !IsCardTypeValid(p.CardType) {
+ p.CardType = CardTypeTextOnly
+ }
+
if !IsTypeValid(p.Type) {
return util.NewInvalidArgumentErrorf("project type is not valid")
}
@@ -280,9 +299,14 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) {
// UpdateProject updates project properties
func UpdateProject(ctx context.Context, p *Project) error {
+ if !IsCardTypeValid(p.CardType) {
+ p.CardType = CardTypeTextOnly
+ }
+
_, err := db.GetEngine(ctx).ID(p.ID).Cols(
"title",
"description",
+ "card_type",
).Update(p)
return err
}
diff --git a/models/project/project_test.go b/models/project/project_test.go
index c2d9005c4..6caa244f5 100644
--- a/models/project/project_test.go
+++ b/models/project/project_test.go
@@ -53,6 +53,7 @@ func TestProject(t *testing.T) {
project := &Project{
Type: TypeRepository,
BoardType: BoardTypeBasicKanban,
+ CardType: CardTypeTextOnly,
Title: "New Project",
RepoID: 1,
CreatedUnix: timeutil.TimeStampNow(),
diff --git a/models/repo/attachment.go b/models/repo/attachment.go
index 8fbf79a7a..cb05386d9 100644
--- a/models/repo/attachment.go
+++ b/models/repo/attachment.go
@@ -132,6 +132,21 @@ func GetAttachmentsByIssueID(ctx context.Context, issueID int64) ([]*Attachment,
return attachments, db.GetEngine(ctx).Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments)
}
+// GetAttachmentsByIssueIDImagesLatest returns the latest image attachments of an issue.
+func GetAttachmentsByIssueIDImagesLatest(ctx context.Context, issueID int64) ([]*Attachment, error) {
+ attachments := make([]*Attachment, 0, 5)
+ return attachments, db.GetEngine(ctx).Where(`issue_id = ? AND (name like '%.apng'
+ OR name like '%.avif'
+ OR name like '%.bmp'
+ OR name like '%.gif'
+ OR name like '%.jpg'
+ OR name like '%.jpeg'
+ OR name like '%.jxl'
+ OR name like '%.png'
+ OR name like '%.svg'
+ OR name like '%.webp')`, issueID).Desc("comment_id").Limit(5).Find(&attachments)
+}
+
// GetAttachmentsByCommentID returns all attachments if comment by given ID.
func GetAttachmentsByCommentID(ctx context.Context, commentID int64) ([]*Attachment, error) {
attachments := make([]*Attachment, 0, 10)
diff --git a/models/repo/avatar.go b/models/repo/avatar.go
index 9ec01bc04..a76a94926 100644
--- a/models/repo/avatar.go
+++ b/models/repo/avatar.go
@@ -85,12 +85,7 @@ func (repo *Repository) relAvatarLink(ctx context.Context) string {
}
// AvatarLink returns a link to the repository's avatar.
-func (repo *Repository) AvatarLink() string {
- return repo.avatarLink(db.DefaultContext)
-}
-
-// avatarLink returns user avatar absolute link.
-func (repo *Repository) avatarLink(ctx context.Context) string {
+func (repo *Repository) AvatarLink(ctx context.Context) string {
link := repo.relAvatarLink(ctx)
// we only prepend our AppURL to our known (relative, internal) avatar link to get an absolute URL
if strings.HasPrefix(link, "/") && !strings.HasPrefix(link, "//") {
diff --git a/models/repo/repo.go b/models/repo/repo.go
index ccc4b8bb1..db85946da 100644
--- a/models/repo/repo.go
+++ b/models/repo/repo.go
@@ -238,7 +238,7 @@ func (repo *Repository) AfterLoad() {
// LoadAttributes loads attributes of the repository.
func (repo *Repository) LoadAttributes(ctx context.Context) error {
// Load owner
- if err := repo.GetOwner(ctx); err != nil {
+ if err := repo.LoadOwner(ctx); err != nil {
return fmt.Errorf("load owner: %w", err)
}
@@ -275,7 +275,7 @@ func (repo *Repository) CommitLink(commitID string) (result string) {
if commitID == "" || commitID == "0000000000000000000000000000000000000000" {
result = ""
} else {
- result = repo.HTMLURL() + "/commit/" + url.PathEscape(commitID)
+ result = repo.Link() + "/commit/" + url.PathEscape(commitID)
}
return result
}
@@ -374,8 +374,8 @@ func (repo *Repository) GetUnit(ctx context.Context, tp unit.Type) (*RepoUnit, e
return nil, ErrUnitTypeNotExist{tp}
}
-// GetOwner returns the repository owner
-func (repo *Repository) GetOwner(ctx context.Context) (err error) {
+// LoadOwner loads owner user
+func (repo *Repository) LoadOwner(ctx context.Context) (err error) {
if repo.Owner != nil {
return nil
}
@@ -389,7 +389,7 @@ func (repo *Repository) GetOwner(ctx context.Context) (err error) {
// It creates a fake object that contains error details
// when error occurs.
func (repo *Repository) MustOwner(ctx context.Context) *user_model.User {
- if err := repo.GetOwner(ctx); err != nil {
+ if err := repo.LoadOwner(ctx); err != nil {
return &user_model.User{
Name: "error",
FullName: err.Error(),
diff --git a/models/repo/repo_unit.go b/models/repo/repo_unit.go
index ee450a46c..7c1af95bf 100644
--- a/models/repo/repo_unit.go
+++ b/models/repo/repo_unit.go
@@ -125,6 +125,7 @@ type PullRequestsConfig struct {
AllowRebaseUpdate bool
DefaultDeleteBranchAfterMerge bool
DefaultMergeStyle MergeStyle
+ DefaultAllowMaintainerEdit bool
}
// FromDB fills up a PullRequestsConfig from serialized format.
diff --git a/models/repo/update.go b/models/repo/update.go
index 8dd109745..f4cb67bb8 100644
--- a/models/repo/update.go
+++ b/models/repo/update.go
@@ -143,7 +143,7 @@ func ChangeRepositoryName(doer *user_model.User, repo *Repository, newRepoName s
return err
}
- if err := repo.GetOwner(db.DefaultContext); err != nil {
+ if err := repo.LoadOwner(db.DefaultContext); err != nil {
return err
}
diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go
index 0d5b8579e..cdb266e01 100644
--- a/models/repo/user_repo.go
+++ b/models/repo/user_repo.go
@@ -61,7 +61,7 @@ func GetWatchedRepos(ctx context.Context, userID int64, private bool, listOption
// GetRepoAssignees returns all users that have write access and can be assigned to issues
// of the repository,
func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.User, err error) {
- if err = repo.GetOwner(ctx); err != nil {
+ if err = repo.LoadOwner(ctx); err != nil {
return nil, err
}
@@ -111,7 +111,7 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
// TODO: may be we should have a busy choice for users to block review request to them.
func GetReviewers(ctx context.Context, repo *Repository, doerID, posterID int64) ([]*user_model.User, error) {
// Get the owner of the repository - this often already pre-cached and if so saves complexity for the following queries
- if err := repo.GetOwner(ctx); err != nil {
+ if err := repo.LoadOwner(ctx); err != nil {
return nil, err
}
diff --git a/models/repo_collaboration_test.go b/models/repo_collaboration_test.go
index 94c5ab529..95fb35fe6 100644
--- a/models/repo_collaboration_test.go
+++ b/models/repo_collaboration_test.go
@@ -17,7 +17,7 @@ func TestRepository_DeleteCollaboration(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
- assert.NoError(t, repo.GetOwner(db.DefaultContext))
+ assert.NoError(t, repo.LoadOwner(db.DefaultContext))
assert.NoError(t, DeleteCollaboration(repo, 4))
unittest.AssertNotExistsBean(t, &repo_model.Collaboration{RepoID: repo.ID, UserID: 4})
diff --git a/models/system/setting.go b/models/system/setting.go
index 50fe17498..098d9a183 100644
--- a/models/system/setting.go
+++ b/models/system/setting.go
@@ -80,8 +80,8 @@ func IsErrDataExpired(err error) bool {
}
// GetSettingNoCache returns specific setting without using the cache
-func GetSettingNoCache(key string) (*Setting, error) {
- v, err := GetSettings([]string{key})
+func GetSettingNoCache(ctx context.Context, key string) (*Setting, error) {
+ v, err := GetSettings(ctx, []string{key})
if err != nil {
return nil, err
}
@@ -91,27 +91,31 @@ func GetSettingNoCache(key string) (*Setting, error) {
return v[strings.ToLower(key)], nil
}
+const contextCacheKey = "system_setting"
+
// GetSetting returns the setting value via the key
-func GetSetting(key string) (string, error) {
- return cache.GetString(genSettingCacheKey(key), func() (string, error) {
- res, err := GetSettingNoCache(key)
- if err != nil {
- return "", err
- }
- return res.SettingValue, nil
+func GetSetting(ctx context.Context, key string) (string, error) {
+ return cache.GetWithContextCache(ctx, contextCacheKey, key, func() (string, error) {
+ return cache.GetString(genSettingCacheKey(key), func() (string, error) {
+ res, err := GetSettingNoCache(ctx, key)
+ if err != nil {
+ return "", err
+ }
+ return res.SettingValue, nil
+ })
})
}
// GetSettingBool return bool value of setting,
// none existing keys and errors are ignored and result in false
-func GetSettingBool(key string) bool {
- s, _ := GetSetting(key)
+func GetSettingBool(ctx context.Context, key string) bool {
+ s, _ := GetSetting(ctx, key)
v, _ := strconv.ParseBool(s)
return v
}
// GetSettings returns specific settings
-func GetSettings(keys []string) (map[string]*Setting, error) {
+func GetSettings(ctx context.Context, keys []string) (map[string]*Setting, error) {
for i := 0; i < len(keys); i++ {
keys[i] = strings.ToLower(keys[i])
}
@@ -161,16 +165,17 @@ func GetAllSettings() (AllSettings, error) {
}
// DeleteSetting deletes a specific setting for a user
-func DeleteSetting(setting *Setting) error {
+func DeleteSetting(ctx context.Context, setting *Setting) error {
+ cache.RemoveContextData(ctx, contextCacheKey, setting.SettingKey)
cache.Remove(genSettingCacheKey(setting.SettingKey))
_, err := db.GetEngine(db.DefaultContext).Delete(setting)
return err
}
-func SetSettingNoVersion(key, value string) error {
- s, err := GetSettingNoCache(key)
+func SetSettingNoVersion(ctx context.Context, key, value string) error {
+ s, err := GetSettingNoCache(ctx, key)
if IsErrSettingIsNotExist(err) {
- return SetSetting(&Setting{
+ return SetSetting(ctx, &Setting{
SettingKey: key,
SettingValue: value,
})
@@ -179,11 +184,11 @@ func SetSettingNoVersion(key, value string) error {
return err
}
s.SettingValue = value
- return SetSetting(s)
+ return SetSetting(ctx, s)
}
// SetSetting updates a users' setting for a specific key
-func SetSetting(setting *Setting) error {
+func SetSetting(ctx context.Context, setting *Setting) error {
if err := upsertSettingValue(strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil {
return err
}
@@ -192,9 +197,11 @@ func SetSetting(setting *Setting) error {
cc := cache.GetCache()
if cc != nil {
- return cc.Put(genSettingCacheKey(setting.SettingKey), setting.SettingValue, setting_module.CacheService.TTLSeconds())
+ if err := cc.Put(genSettingCacheKey(setting.SettingKey), setting.SettingValue, setting_module.CacheService.TTLSeconds()); err != nil {
+ return err
+ }
}
-
+ cache.SetContextData(ctx, contextCacheKey, setting.SettingKey, setting.SettingValue)
return nil
}
@@ -244,7 +251,7 @@ var (
func Init() error {
var disableGravatar bool
- disableGravatarSetting, err := GetSettingNoCache(KeyPictureDisableGravatar)
+ disableGravatarSetting, err := GetSettingNoCache(db.DefaultContext, KeyPictureDisableGravatar)
if IsErrSettingIsNotExist(err) {
disableGravatar = setting_module.GetDefaultDisableGravatar()
disableGravatarSetting = &Setting{SettingValue: strconv.FormatBool(disableGravatar)}
@@ -255,7 +262,7 @@ func Init() error {
}
var enableFederatedAvatar bool
- enableFederatedAvatarSetting, err := GetSettingNoCache(KeyPictureEnableFederatedAvatar)
+ enableFederatedAvatarSetting, err := GetSettingNoCache(db.DefaultContext, KeyPictureEnableFederatedAvatar)
if IsErrSettingIsNotExist(err) {
enableFederatedAvatar = setting_module.GetDefaultEnableFederatedAvatar(disableGravatar)
enableFederatedAvatarSetting = &Setting{SettingValue: strconv.FormatBool(enableFederatedAvatar)}
@@ -268,13 +275,13 @@ func Init() error {
if setting_module.OfflineMode {
disableGravatar = true
enableFederatedAvatar = false
- if !GetSettingBool(KeyPictureDisableGravatar) {
- if err := SetSettingNoVersion(KeyPictureDisableGravatar, "true"); err != nil {
+ if !GetSettingBool(db.DefaultContext, KeyPictureDisableGravatar) {
+ if err := SetSettingNoVersion(db.DefaultContext, KeyPictureDisableGravatar, "true"); err != nil {
return fmt.Errorf("Failed to set setting %q: %w", KeyPictureDisableGravatar, err)
}
}
- if GetSettingBool(KeyPictureEnableFederatedAvatar) {
- if err := SetSettingNoVersion(KeyPictureEnableFederatedAvatar, "false"); err != nil {
+ if GetSettingBool(db.DefaultContext, KeyPictureEnableFederatedAvatar) {
+ if err := SetSettingNoVersion(db.DefaultContext, KeyPictureEnableFederatedAvatar, "false"); err != nil {
return fmt.Errorf("Failed to set setting %q: %w", KeyPictureEnableFederatedAvatar, err)
}
}
diff --git a/models/system/setting_test.go b/models/system/setting_test.go
index c43d2e308..fbd04088e 100644
--- a/models/system/setting_test.go
+++ b/models/system/setting_test.go
@@ -7,6 +7,7 @@ import (
"strings"
"testing"
+ "code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/models/unittest"
@@ -20,24 +21,24 @@ func TestSettings(t *testing.T) {
newSetting := &system.Setting{SettingKey: keyName, SettingValue: "50"}
// create setting
- err := system.SetSetting(newSetting)
+ err := system.SetSetting(db.DefaultContext, newSetting)
assert.NoError(t, err)
// test about saving unchanged values
- err = system.SetSetting(newSetting)
+ err = system.SetSetting(db.DefaultContext, newSetting)
assert.NoError(t, err)
// get specific setting
- settings, err := system.GetSettings([]string{keyName})
+ settings, err := system.GetSettings(db.DefaultContext, []string{keyName})
assert.NoError(t, err)
assert.Len(t, settings, 1)
assert.EqualValues(t, newSetting.SettingValue, settings[strings.ToLower(keyName)].SettingValue)
// updated setting
updatedSetting := &system.Setting{SettingKey: keyName, SettingValue: "100", Version: settings[strings.ToLower(keyName)].Version}
- err = system.SetSetting(updatedSetting)
+ err = system.SetSetting(db.DefaultContext, updatedSetting)
assert.NoError(t, err)
- value, err := system.GetSetting(keyName)
+ value, err := system.GetSetting(db.DefaultContext, keyName)
assert.NoError(t, err)
assert.EqualValues(t, updatedSetting.SettingValue, value)
@@ -48,7 +49,7 @@ func TestSettings(t *testing.T) {
assert.EqualValues(t, updatedSetting.SettingValue, settings[strings.ToLower(updatedSetting.SettingKey)].SettingValue)
// delete setting
- err = system.DeleteSetting(&system.Setting{SettingKey: strings.ToLower(keyName)})
+ err = system.DeleteSetting(db.DefaultContext, &system.Setting{SettingKey: strings.ToLower(keyName)})
assert.NoError(t, err)
settings, err = system.GetAllSettings()
assert.NoError(t, err)
diff --git a/models/unittest/fixtures.go b/models/unittest/fixtures.go
index 9fba05382..545452a15 100644
--- a/models/unittest/fixtures.go
+++ b/models/unittest/fixtures.go
@@ -9,6 +9,8 @@ import (
"time"
"code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/auth/password/hash"
+ "code.gitea.io/gitea/modules/setting"
"github.com/go-testfixtures/testfixtures/v3"
"xorm.io/xorm"
@@ -64,6 +66,11 @@ func InitFixtures(opts FixturesOptions, engine ...*xorm.Engine) (err error) {
return err
}
+ // register the dummy hash algorithm function used in the test fixtures
+ _ = hash.Register("dummy", hash.NewDummyHasher)
+
+ setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
+
return err
}
@@ -115,5 +122,8 @@ func LoadFixtures(engine ...*xorm.Engine) error {
}
}
}
+ _ = hash.Register("dummy", hash.NewDummyHasher)
+ setting.PasswordHashAlgo, _ = hash.SetDefaultPasswordHashAlgorithm("dummy")
+
return err
}
diff --git a/models/user.go b/models/user.go
deleted file mode 100644
index 746553c35..000000000
--- a/models/user.go
+++ /dev/null
@@ -1,189 +0,0 @@
-// Copyright 2014 The Gogs Authors. All rights reserved.
-// Copyright 2019 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package models
-
-import (
- "context"
- "fmt"
- "time"
-
- _ "image/jpeg" // Needed for jpeg support
-
- activities_model "code.gitea.io/gitea/models/activities"
- asymkey_model "code.gitea.io/gitea/models/asymkey"
- auth_model "code.gitea.io/gitea/models/auth"
- "code.gitea.io/gitea/models/db"
- git_model "code.gitea.io/gitea/models/git"
- issues_model "code.gitea.io/gitea/models/issues"
- "code.gitea.io/gitea/models/organization"
- access_model "code.gitea.io/gitea/models/perm/access"
- pull_model "code.gitea.io/gitea/models/pull"
- repo_model "code.gitea.io/gitea/models/repo"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/setting"
-)
-
-// DeleteUser deletes models associated to an user.
-func DeleteUser(ctx context.Context, u *user_model.User, purge bool) (err error) {
- e := db.GetEngine(ctx)
-
- // ***** START: Watch *****
- watchedRepoIDs := make([]int64, 0, 10)
- if err = e.Table("watch").Cols("watch.repo_id").
- Where("watch.user_id = ?", u.ID).And("watch.mode <>?", repo_model.WatchModeDont).Find(&watchedRepoIDs); err != nil {
- return fmt.Errorf("get all watches: %w", err)
- }
- if _, err = e.Decr("num_watches").In("id", watchedRepoIDs).NoAutoTime().Update(new(repo_model.Repository)); err != nil {
- return fmt.Errorf("decrease repository num_watches: %w", err)
- }
- // ***** END: Watch *****
-
- // ***** START: Star *****
- starredRepoIDs := make([]int64, 0, 10)
- if err = e.Table("star").Cols("star.repo_id").
- Where("star.uid = ?", u.ID).Find(&starredRepoIDs); err != nil {
- return fmt.Errorf("get all stars: %w", err)
- } else if _, err = e.Decr("num_stars").In("id", starredRepoIDs).NoAutoTime().Update(new(repo_model.Repository)); err != nil {
- return fmt.Errorf("decrease repository num_stars: %w", err)
- }
- // ***** END: Star *****
-
- // ***** START: Follow *****
- followeeIDs := make([]int64, 0, 10)
- if err = e.Table("follow").Cols("follow.follow_id").
- Where("follow.user_id = ?", u.ID).Find(&followeeIDs); err != nil {
- return fmt.Errorf("get all followees: %w", err)
- } else if _, err = e.Decr("num_followers").In("id", followeeIDs).Update(new(user_model.User)); err != nil {
- return fmt.Errorf("decrease user num_followers: %w", err)
- }
-
- followerIDs := make([]int64, 0, 10)
- if err = e.Table("follow").Cols("follow.user_id").
- Where("follow.follow_id = ?", u.ID).Find(&followerIDs); err != nil {
- return fmt.Errorf("get all followers: %w", err)
- } else if _, err = e.Decr("num_following").In("id", followerIDs).Update(new(user_model.User)); err != nil {
- return fmt.Errorf("decrease user num_following: %w", err)
- }
- // ***** END: Follow *****
-
- if err = db.DeleteBeans(ctx,
- &auth_model.AccessToken{UID: u.ID},
- &repo_model.Collaboration{UserID: u.ID},
- &access_model.Access{UserID: u.ID},
- &repo_model.Watch{UserID: u.ID},
- &repo_model.Star{UID: u.ID},
- &user_model.Follow{UserID: u.ID},
- &user_model.Follow{FollowID: u.ID},
- &activities_model.Action{UserID: u.ID},
- &issues_model.IssueUser{UID: u.ID},
- &user_model.EmailAddress{UID: u.ID},
- &user_model.UserOpenID{UID: u.ID},
- &issues_model.Reaction{UserID: u.ID},
- &organization.TeamUser{UID: u.ID},
- &issues_model.Stopwatch{UserID: u.ID},
- &user_model.Setting{UserID: u.ID},
- &user_model.UserBadge{UserID: u.ID},
- &pull_model.AutoMerge{DoerID: u.ID},
- &pull_model.ReviewState{UserID: u.ID},
- &user_model.Redirect{RedirectUserID: u.ID},
- ); err != nil {
- return fmt.Errorf("deleteBeans: %w", err)
- }
-
- if err := auth_model.DeleteOAuth2RelictsByUserID(ctx, u.ID); err != nil {
- return err
- }
-
- if purge || (setting.Service.UserDeleteWithCommentsMaxTime != 0 &&
- u.CreatedUnix.AsTime().Add(setting.Service.UserDeleteWithCommentsMaxTime).After(time.Now())) {
-
- // Delete Comments
- const batchSize = 50
- for {
- comments := make([]*issues_model.Comment, 0, batchSize)
- if err = e.Where("type=? AND poster_id=?", issues_model.CommentTypeComment, u.ID).Limit(batchSize, 0).Find(&comments); err != nil {
- return err
- }
- if len(comments) == 0 {
- break
- }
-
- for _, comment := range comments {
- if err = issues_model.DeleteComment(ctx, comment); err != nil {
- return err
- }
- }
- }
-
- // Delete Reactions
- if err = issues_model.DeleteReaction(ctx, &issues_model.ReactionOptions{DoerID: u.ID}); err != nil {
- return err
- }
- }
-
- // ***** START: Branch Protections *****
- {
- const batchSize = 50
- for start := 0; ; start += batchSize {
- protections := make([]*git_model.ProtectedBranch, 0, batchSize)
- // @perf: We can't filter on DB side by u.ID, as those IDs are serialized as JSON strings.
- // We could filter down with `WHERE repo_id IN (reposWithPushPermission(u))`,
- // though that query will be quite complex and tricky to maintain (compare `getRepoAssignees()`).
- // Also, as we didn't update branch protections when removing entries from `access` table,
- // it's safer to iterate all protected branches.
- if err = e.Limit(batchSize, start).Find(&protections); err != nil {
- return fmt.Errorf("findProtectedBranches: %w", err)
- }
- if len(protections) == 0 {
- break
- }
- for _, p := range protections {
- if err := git_model.RemoveUserIDFromProtectedBranch(ctx, p, u.ID); err != nil {
- return err
- }
- }
- }
- }
- // ***** END: Branch Protections *****
-
- // ***** START: PublicKey *****
- if _, err = db.DeleteByBean(ctx, &asymkey_model.PublicKey{OwnerID: u.ID}); err != nil {
- return fmt.Errorf("deletePublicKeys: %w", err)
- }
- // ***** END: PublicKey *****
-
- // ***** START: GPGPublicKey *****
- keys, err := asymkey_model.ListGPGKeys(ctx, u.ID, db.ListOptions{})
- if err != nil {
- return fmt.Errorf("ListGPGKeys: %w", err)
- }
- // Delete GPGKeyImport(s).
- for _, key := range keys {
- if _, err = db.DeleteByBean(ctx, &asymkey_model.GPGKeyImport{KeyID: key.KeyID}); err != nil {
- return fmt.Errorf("deleteGPGKeyImports: %w", err)
- }
- }
- if _, err = db.DeleteByBean(ctx, &asymkey_model.GPGKey{OwnerID: u.ID}); err != nil {
- return fmt.Errorf("deleteGPGKeys: %w", err)
- }
- // ***** END: GPGPublicKey *****
-
- // Clear assignee.
- if _, err = db.DeleteByBean(ctx, &issues_model.IssueAssignees{AssigneeID: u.ID}); err != nil {
- return fmt.Errorf("clear assignee: %w", err)
- }
-
- // ***** START: ExternalLoginUser *****
- if err = user_model.RemoveAllAccountLinks(ctx, u); err != nil {
- return fmt.Errorf("ExternalLoginUser: %w", err)
- }
- // ***** END: ExternalLoginUser *****
-
- if _, err = e.ID(u.ID).Delete(new(user_model.User)); err != nil {
- return fmt.Errorf("delete: %w", err)
- }
-
- return nil
-}
diff --git a/models/user/avatar.go b/models/user/avatar.go
index 4a4ec2fe8..f08757b6a 100644
--- a/models/user/avatar.go
+++ b/models/user/avatar.go
@@ -58,7 +58,7 @@ func GenerateRandomAvatar(ctx context.Context, u *User) error {
}
// AvatarLinkWithSize returns a link to the user's avatar with size. size <= 0 means default size
-func (u *User) AvatarLinkWithSize(size int) string {
+func (u *User) AvatarLinkWithSize(ctx context.Context, size int) string {
if u.ID == -1 {
// ghost user
return avatars.DefaultAvatarLink()
@@ -67,7 +67,7 @@ func (u *User) AvatarLinkWithSize(size int) string {
useLocalAvatar := false
autoGenerateAvatar := false
- disableGravatar := system_model.GetSettingBool(system_model.KeyPictureDisableGravatar)
+ disableGravatar := system_model.GetSettingBool(ctx, system_model.KeyPictureDisableGravatar)
switch {
case u.UseCustomAvatar:
@@ -79,7 +79,7 @@ func (u *User) AvatarLinkWithSize(size int) string {
if useLocalAvatar {
if u.Avatar == "" && autoGenerateAvatar {
- if err := GenerateRandomAvatar(db.DefaultContext, u); err != nil {
+ if err := GenerateRandomAvatar(ctx, u); err != nil {
log.Error("GenerateRandomAvatar: %v", err)
}
}
@@ -88,12 +88,12 @@ func (u *User) AvatarLinkWithSize(size int) string {
}
return avatars.GenerateUserAvatarImageLink(u.Avatar, size)
}
- return avatars.GenerateEmailAvatarFastLink(u.AvatarEmail, size)
+ return avatars.GenerateEmailAvatarFastLink(ctx, u.AvatarEmail, size)
}
// AvatarLink returns the full avatar link with http host
-func (u *User) AvatarLink() string {
- link := u.AvatarLinkWithSize(0)
+func (u *User) AvatarLink(ctx context.Context) string {
+ link := u.AvatarLinkWithSize(ctx, 0)
if !strings.HasPrefix(link, "//") && !strings.Contains(link, "://") {
return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL+"/")
}
@@ -101,8 +101,8 @@ func (u *User) AvatarLink() string {
}
// AvatarFullLinkWithSize returns the full avatar link with size and http host
-func (u *User) AvatarFullLinkWithSize(size int) string {
- link := u.AvatarLinkWithSize(size)
+func (u *User) AvatarFullLinkWithSize(ctx context.Context, size int) string {
+ link := u.AvatarLinkWithSize(ctx, size)
if !strings.HasPrefix(link, "//") && !strings.Contains(link, "://") {
return setting.AppURL + strings.TrimPrefix(link, setting.AppSubURL+"/")
}
diff --git a/models/user/must_change_password.go b/models/user/must_change_password.go
new file mode 100644
index 000000000..7eab08de8
--- /dev/null
+++ b/models/user/must_change_password.go
@@ -0,0 +1,49 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+ "context"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/util"
+
+ "xorm.io/builder"
+)
+
+func SetMustChangePassword(ctx context.Context, all, mustChangePassword bool, include, exclude []string) (int64, error) {
+ sliceTrimSpaceDropEmpty := func(input []string) []string {
+ output := make([]string, 0, len(input))
+ for _, in := range input {
+ in = strings.ToLower(strings.TrimSpace(in))
+ if in == "" {
+ continue
+ }
+ output = append(output, in)
+ }
+ return output
+ }
+
+ var cond builder.Cond
+
+ // Only include the users where something changes to get an accurate count
+ cond = builder.Neq{"must_change_password": mustChangePassword}
+
+ if !all {
+ include = sliceTrimSpaceDropEmpty(include)
+ if len(include) == 0 {
+ return 0, util.NewSilentWrapErrorf(util.ErrInvalidArgument, "no users to include provided")
+ }
+
+ cond = cond.And(builder.In("lower_name", include))
+ }
+
+ exclude = sliceTrimSpaceDropEmpty(exclude)
+ if len(exclude) > 0 {
+ cond = cond.And(builder.NotIn("lower_name", exclude))
+ }
+
+ return db.GetEngine(ctx).Where(cond).MustCols("must_change_password").Update(&User{MustChangePassword: mustChangePassword})
+}
diff --git a/models/user/user.go b/models/user/user.go
index fe44868e7..3a2e4f19b 100644
--- a/models/user/user.go
+++ b/models/user/user.go
@@ -6,8 +6,6 @@ package user
import (
"context"
- "crypto/sha256"
- "crypto/subtle"
"encoding/hex"
"errors"
"fmt"
@@ -22,6 +20,7 @@ import (
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/auth/openid"
+ "code.gitea.io/gitea/modules/auth/password/hash"
"code.gitea.io/gitea/modules/base"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
@@ -31,10 +30,6 @@ import (
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"
- "golang.org/x/crypto/argon2"
- "golang.org/x/crypto/bcrypt"
- "golang.org/x/crypto/pbkdf2"
- "golang.org/x/crypto/scrypt"
"xorm.io/builder"
)
@@ -50,21 +45,6 @@ const (
)
const (
- algoBcrypt = "bcrypt"
- algoScrypt = "scrypt"
- algoArgon2 = "argon2"
- algoPbkdf2 = "pbkdf2"
-)
-
-// AvailableHashAlgorithms represents the available password hashing algorithms
-var AvailableHashAlgorithms = []string{
- algoPbkdf2,
- algoArgon2,
- algoScrypt,
- algoBcrypt,
-}
-
-const (
// EmailNotificationsEnabled indicates that the user would like to receive all email notifications except your own
EmailNotificationsEnabled = "enabled"
// EmailNotificationsOnMention indicates that the user would like to be notified via email when mentioned.
@@ -378,42 +358,6 @@ func (u *User) NewGitSig() *git.Signature {
}
}
-func hashPassword(passwd, salt, algo string) (string, error) {
- var tempPasswd []byte
- var saltBytes []byte
-
- // There are two formats for the Salt value:
- // * The new format is a (32+)-byte hex-encoded string
- // * The old format was a 10-byte binary format
- // We have to tolerate both here but Authenticate should
- // regenerate the Salt following a successful validation.
- if len(salt) == 10 {
- saltBytes = []byte(salt)
- } else {
- var err error
- saltBytes, err = hex.DecodeString(salt)
- if err != nil {
- return "", err
- }
- }
-
- switch algo {
- case algoBcrypt:
- tempPasswd, _ = bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost)
- return string(tempPasswd), nil
- case algoScrypt:
- tempPasswd, _ = scrypt.Key([]byte(passwd), saltBytes, 65536, 16, 2, 50)
- case algoArgon2:
- tempPasswd = argon2.IDKey([]byte(passwd), saltBytes, 2, 65536, 8, 50)
- case algoPbkdf2:
- fallthrough
- default:
- tempPasswd = pbkdf2.Key([]byte(passwd), saltBytes, 10000, 50, sha256.New)
- }
-
- return hex.EncodeToString(tempPasswd), nil
-}
-
// SetPassword hashes a password using the algorithm defined in the config value of PASSWORD_HASH_ALGO
// change passwd, salt and passwd_hash_algo fields
func (u *User) SetPassword(passwd string) (err error) {
@@ -427,7 +371,7 @@ func (u *User) SetPassword(passwd string) (err error) {
if u.Salt, err = GetUserSalt(); err != nil {
return err
}
- if u.Passwd, err = hashPassword(passwd, u.Salt, setting.PasswordHashAlgo); err != nil {
+ if u.Passwd, err = hash.Parse(setting.PasswordHashAlgo).Hash(passwd, u.Salt); err != nil {
return err
}
u.PasswdHashAlgo = setting.PasswordHashAlgo
@@ -435,20 +379,9 @@ func (u *User) SetPassword(passwd string) (err error) {
return nil
}
-// ValidatePassword checks if given password matches the one belongs to the user.
+// ValidatePassword checks if the given password matches the one belonging to the user.
func (u *User) ValidatePassword(passwd string) bool {
- tempHash, err := hashPassword(passwd, u.Salt, u.PasswdHashAlgo)
- if err != nil {
- return false
- }
-
- if u.PasswdHashAlgo != algoBcrypt && subtle.ConstantTimeCompare([]byte(u.Passwd), []byte(tempHash)) == 1 {
- return true
- }
- if u.PasswdHashAlgo == algoBcrypt && bcrypt.CompareHashAndPassword([]byte(u.Passwd), []byte(passwd)) == nil {
- return true
- }
- return false
+ return hash.Parse(u.PasswdHashAlgo).VerifyPassword(passwd, u.Passwd, u.Salt)
}
// IsPasswordSet checks if the password is set or left empty
@@ -641,6 +574,11 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
u.IsRestricted = setting.Service.DefaultUserIsRestricted
u.IsActive = !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm)
+ // Ensure consistency of the dates.
+ if u.UpdatedUnix < u.CreatedUnix {
+ u.UpdatedUnix = u.CreatedUnix
+ }
+
// overwrite defaults if set
if len(overwriteDefault) != 0 && overwriteDefault[0] != nil {
overwrite := overwriteDefault[0]
@@ -718,7 +656,15 @@ func CreateUser(u *User, overwriteDefault ...*CreateUserOverwriteOptions) (err e
return err
}
- if err = db.Insert(ctx, u); err != nil {
+ if u.CreatedUnix == 0 {
+ // Caller expects auto-time for creation & update timestamps.
+ err = db.Insert(ctx, u)
+ } else {
+ // Caller sets the timestamps themselves. They are responsible for ensuring
+ // both `CreatedUnix` and `UpdatedUnix` are set appropriately.
+ _, err = db.GetEngine(ctx).NoAutoTime().Insert(u)
+ }
+ if err != nil {
return err
}
@@ -1138,11 +1084,11 @@ type UserCommit struct { //revive:disable-line:exported
}
// ValidateCommitWithEmail check if author's e-mail of commit is corresponding to a user.
-func ValidateCommitWithEmail(c *git.Commit) *User {
+func ValidateCommitWithEmail(ctx context.Context, c *git.Commit) *User {
if c.Author == nil {
return nil
}
- u, err := GetUserByEmail(c.Author.Email)
+ u, err := GetUserByEmail(ctx, c.Author.Email)
if err != nil {
return nil
}
@@ -1150,7 +1096,7 @@ func ValidateCommitWithEmail(c *git.Commit) *User {
}
// ValidateCommitsWithEmails checks if authors' e-mails of commits are corresponding to users.
-func ValidateCommitsWithEmails(oldCommits []*git.Commit) []*UserCommit {
+func ValidateCommitsWithEmails(ctx context.Context, oldCommits []*git.Commit) []*UserCommit {
var (
emails = make(map[string]*User)
newCommits = make([]*UserCommit, 0, len(oldCommits))
@@ -1159,7 +1105,7 @@ func ValidateCommitsWithEmails(oldCommits []*git.Commit) []*UserCommit {
var u *User
if c.Author != nil {
if v, ok := emails[c.Author.Email]; !ok {
- u, _ = GetUserByEmail(c.Author.Email)
+ u, _ = GetUserByEmail(ctx, c.Author.Email)
emails[c.Author.Email] = u
} else {
u = v
@@ -1175,12 +1121,7 @@ func ValidateCommitsWithEmails(oldCommits []*git.Commit) []*UserCommit {
}
// GetUserByEmail returns the user object by given e-mail if exists.
-func GetUserByEmail(email string) (*User, error) {
- return GetUserByEmailContext(db.DefaultContext, email)
-}
-
-// GetUserByEmailContext returns the user object by given e-mail if exists with db context
-func GetUserByEmailContext(ctx context.Context, email string) (*User, error) {
+func GetUserByEmail(ctx context.Context, email string) (*User, error) {
if len(email) == 0 {
return nil, ErrUserNotExist{0, email, 0}
}
diff --git a/models/user/user_test.go b/models/user/user_test.go
index 525da531f..fc8f6b8d5 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -4,16 +4,20 @@
package user_test
import (
+ "context"
"math/rand"
"strings"
"testing"
+ "time"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/auth/password/hash"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
+ "code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert"
@@ -161,7 +165,7 @@ func TestEmailNotificationPreferences(t *testing.T) {
func TestHashPasswordDeterministic(t *testing.T) {
b := make([]byte, 16)
u := &user_model.User{}
- algos := []string{"argon2", "pbkdf2", "scrypt", "bcrypt"}
+ algos := hash.RecommendedHashAlgorithms
for j := 0; j < len(algos); j++ {
u.PasswdHashAlgo = algos[j]
for i := 0; i < 50; i++ {
@@ -252,6 +256,58 @@ func TestCreateUserEmailAlreadyUsed(t *testing.T) {
assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
}
+func TestCreateUserCustomTimestamps(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ // Add new user with a custom creation timestamp.
+ var creationTimestamp timeutil.TimeStamp = 12345
+ user.Name = "testuser"
+ user.LowerName = strings.ToLower(user.Name)
+ user.ID = 0
+ user.Email = "unique@example.com"
+ user.CreatedUnix = creationTimestamp
+ err := user_model.CreateUser(user)
+ assert.NoError(t, err)
+
+ fetched, err := user_model.GetUserByID(context.Background(), user.ID)
+ assert.NoError(t, err)
+ assert.Equal(t, creationTimestamp, fetched.CreatedUnix)
+ assert.Equal(t, creationTimestamp, fetched.UpdatedUnix)
+}
+
+func TestCreateUserWithoutCustomTimestamps(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+
+ // There is no way to use a mocked time for the XORM auto-time functionality,
+ // so use the real clock to approximate the expected timestamp.
+ timestampStart := time.Now().Unix()
+
+ // Add new user without a custom creation timestamp.
+ user.Name = "Testuser"
+ user.LowerName = strings.ToLower(user.Name)
+ user.ID = 0
+ user.Email = "unique@example.com"
+ user.CreatedUnix = 0
+ user.UpdatedUnix = 0
+ err := user_model.CreateUser(user)
+ assert.NoError(t, err)
+
+ timestampEnd := time.Now().Unix()
+
+ fetched, err := user_model.GetUserByID(context.Background(), user.ID)
+ assert.NoError(t, err)
+
+ assert.LessOrEqual(t, timestampStart, fetched.CreatedUnix)
+ assert.LessOrEqual(t, fetched.CreatedUnix, timestampEnd)
+
+ assert.LessOrEqual(t, timestampStart, fetched.UpdatedUnix)
+ assert.LessOrEqual(t, fetched.UpdatedUnix, timestampEnd)
+}
+
func TestGetUserIDsByNames(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())