diff options
author | Anthony Wang | 2023-02-20 22:21:24 +0000 |
---|---|---|
committer | Anthony Wang | 2023-02-20 22:21:24 +0000 |
commit | dc20c2832871f6462990751ea802e14b02bf41b0 (patch) | |
tree | 71bfef0694e6c0f8284438c290521d7297f24eab /models | |
parent | 07df0a6b1c97be4b03d23d5dfa047a108de36592 (diff) | |
parent | ef11d41639dd1e89676e395068ee453312560adb (diff) |
Merge remote-tracking branch 'origin/main' into forgejo-federation
Diffstat (limited to 'models')
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()) |