-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Add k/v store for repo settings #22500
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
69c8e6e
improvements
lunny 8cead1a
Use error wrap and fix test
lunny e5434d4
Fix copyright year
lunny d6b5d37
share the common function
lunny 6d80c9b
Merge branch 'main' into lunny/repo_settings
lunny bdeb171
merge main branch
lunny 3ee92eb
Use an abstract db.ResourceSetting like what db.ResourceIndex did
lunny 9f7e438
Merge branch 'main' into lunny/repo_settings
lunny bdb1547
Merge branch 'main' into lunny/repo_settings
lunny a7490f2
merge main branch
lunny c16f05b
Merge branch 'lunny/repo_settings' of github.com:lunny/gitea into lun…
lunny 22f58d7
Merge branch 'main' into lunny/repo_settings
lunny 9deb488
Merge branch 'main' into lunny/repo_settings
6543 5e0d910
merge main branch
lunny 29837cc
Merge branch 'lunny/repo_settings' of github.com:lunny/gitea into lun…
lunny 7acebb1
follow @delvh suggestion
lunny 0fdd601
merge main branch
lunny File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// Copyright 2022 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package v1_19 //nolint | ||
|
||
import ( | ||
"code.gitea.io/gitea/modules/timeutil" | ||
|
||
"xorm.io/xorm" | ||
) | ||
|
||
func CreateRepoSettingsTable(x *xorm.Engine) error { | ||
type RepoSetting struct { | ||
ID int64 `xorm:"pk autoincr"` | ||
RepoID int64 `xorm:"index unique(key_repoid)"` // to load all of someone's settings | ||
SettingKey string `xorm:"varchar(255) index unique(key_repoid)"` // ensure key is always lowercase | ||
SettingValue string `xorm:"text"` | ||
Version int `xorm:"version"` // prevent to override | ||
Created timeutil.TimeStamp `xorm:"created"` | ||
Updated timeutil.TimeStamp `xorm:"updated"` | ||
} | ||
return x.Sync2(new(RepoSetting)) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
// Copyright 2022 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package repo | ||
lunny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
|
||
"code.gitea.io/gitea/models/db" | ||
"code.gitea.io/gitea/modules/cache" | ||
setting_module "code.gitea.io/gitea/modules/setting" | ||
"code.gitea.io/gitea/modules/timeutil" | ||
|
||
"xorm.io/builder" | ||
) | ||
|
||
// genSettingCacheKey returns the cache key for some configuration | ||
func genSettingCacheKey(repoID int64, key string) string { | ||
return fmt.Sprintf("repo.setting.%d.%s", repoID, key) | ||
} | ||
|
||
// Setting is a key value store of repo settings | ||
type Setting struct { | ||
ID int64 `xorm:"pk autoincr"` | ||
RepoID int64 `xorm:"index unique(key_repoid)"` // to load all of someone's settings | ||
SettingKey string `xorm:"varchar(255) index unique(key_repoid)"` // ensure key is always lowercase | ||
SettingValue string `xorm:"text"` | ||
Version int `xorm:"version"` // prevent to override | ||
Created timeutil.TimeStamp `xorm:"created"` | ||
Updated timeutil.TimeStamp `xorm:"updated"` | ||
} | ||
|
||
// TableName sets the table name for the settings struct | ||
func (s *Setting) TableName() string { | ||
return "repo_setting" | ||
} | ||
|
||
func (s *Setting) GetValueBool() bool { | ||
lunny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if s == nil { | ||
return false | ||
} | ||
|
||
b, _ := strconv.ParseBool(s.SettingValue) | ||
return b | ||
} | ||
|
||
func init() { | ||
db.RegisterModel(new(Setting)) | ||
} | ||
|
||
// ErrSettingIsNotExist represents an error that a setting is not exist with special key | ||
type ErrSettingIsNotExist struct { | ||
lunny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
RepoID int64 | ||
Key string | ||
} | ||
|
||
// Error implements error | ||
func (err ErrSettingIsNotExist) Error() string { | ||
return fmt.Sprintf("Repo[%d] setting[%s] is not exist", err.RepoID, err.Key) | ||
} | ||
|
||
// IsErrSettingIsNotExist return true if err is ErrSettingIsNotExist | ||
func IsErrSettingIsNotExist(err error) bool { | ||
_, ok := err.(ErrSettingIsNotExist) | ||
return ok | ||
} | ||
|
||
// GetSettingNoCache returns specific setting without using the cache | ||
func GetSettingNoCache(repoID int64, key string) (*Setting, error) { | ||
v, err := GetSettings(repoID, []string{key}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if len(v) == 0 { | ||
return nil, ErrSettingIsNotExist{repoID, key} | ||
} | ||
return v[strings.ToLower(key)], nil | ||
} | ||
|
||
func validateRepoSettingKey(key string) error { | ||
if len(key) == 0 { | ||
return fmt.Errorf("setting key must be set") | ||
} | ||
if strings.ToLower(key) != key { | ||
return fmt.Errorf("setting key should be lowercase") | ||
} | ||
return nil | ||
} | ||
lunny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// GetSetting returns the setting value via the key | ||
func GetSetting(repoID int64, key string) (string, error) { | ||
if err := validateRepoSettingKey(key); err != nil { | ||
return "", err | ||
} | ||
return cache.GetString(genSettingCacheKey(repoID, key), func() (string, error) { | ||
res, err := GetSettingNoCache(repoID, 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(repoID int64, key string) bool { | ||
s, _ := GetSetting(repoID, key) | ||
v, _ := strconv.ParseBool(s) | ||
return v | ||
} | ||
|
||
// GetSettings returns specific settings | ||
func GetSettings(repoID int64, keys []string) (map[string]*Setting, error) { | ||
for i := 0; i < len(keys); i++ { | ||
keys[i] = strings.ToLower(keys[i]) | ||
} | ||
settings := make([]*Setting, 0, len(keys)) | ||
if err := db.GetEngine(db.DefaultContext). | ||
Where("repo_id=?", repoID). | ||
And(builder.In("setting_key", keys)). | ||
Find(&settings); err != nil { | ||
return nil, err | ||
} | ||
settingsMap := make(map[string]*Setting) | ||
for _, s := range settings { | ||
settingsMap[s.SettingKey] = s | ||
} | ||
return settingsMap, nil | ||
} | ||
|
||
type AllSettings map[string]*Setting | ||
|
||
func (settings AllSettings) Get(key string) Setting { | ||
if v, ok := settings[strings.ToLower(key)]; ok { | ||
return *v | ||
} | ||
return Setting{} | ||
} | ||
|
||
func (settings AllSettings) GetBool(key string) bool { | ||
b, _ := strconv.ParseBool(settings.Get(key).SettingValue) | ||
return b | ||
} | ||
|
||
func (settings AllSettings) GetVersion(key string) int { | ||
return settings.Get(key).Version | ||
} | ||
|
||
// GetAllSettings returns all settings from repo | ||
func GetAllSettings(repoID int64) (AllSettings, error) { | ||
settings := make([]*Setting, 0, 5) | ||
if err := db.GetEngine(db.DefaultContext). | ||
Where("repo_id=?", repoID). | ||
Find(&settings); err != nil { | ||
return nil, err | ||
} | ||
settingsMap := make(map[string]*Setting) | ||
for _, s := range settings { | ||
settingsMap[s.SettingKey] = s | ||
} | ||
return settingsMap, nil | ||
} | ||
|
||
// DeleteSetting deletes a specific setting for a repo | ||
func DeleteSetting(repoID int64, key string) error { | ||
lunny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if err := validateRepoSettingKey(key); err != nil { | ||
return err | ||
} | ||
cache.Remove(genSettingCacheKey(repoID, key)) | ||
_, err := db.GetEngine(db.DefaultContext).Delete(&Setting{RepoID: repoID, SettingKey: key}) | ||
return err | ||
} | ||
|
||
func SetSettingNoVersion(repoID int64, key, value string) error { | ||
s, err := GetSettingNoCache(repoID, key) | ||
if IsErrSettingIsNotExist(err) { | ||
return SetSetting(&Setting{ | ||
RepoID: repoID, | ||
SettingKey: key, | ||
SettingValue: value, | ||
}) | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
s.SettingValue = value | ||
return SetSetting(s) | ||
} | ||
|
||
// SetSetting updates a users' setting for a specific key | ||
func SetSetting(setting *Setting) error { | ||
if err := upsertSettingValue(setting.RepoID, strings.ToLower(setting.SettingKey), setting.SettingValue, setting.Version); err != nil { | ||
return err | ||
} | ||
|
||
setting.Version++ | ||
|
||
cc := cache.GetCache() | ||
if cc != nil { | ||
return cc.Put(genSettingCacheKey(setting.RepoID, setting.SettingKey), setting.SettingValue, setting_module.CacheService.TTLSeconds()) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func upsertSettingValue(repoID int64, key, value string, version int) error { | ||
return db.WithTx(db.DefaultContext, func(ctx context.Context) error { | ||
e := db.GetEngine(ctx) | ||
|
||
// here we use a general method to do a safe upsert for different databases (and most transaction levels) | ||
// 1. try to UPDATE the record and acquire the transaction write lock | ||
// if UPDATE returns non-zero rows are changed, OK, the setting is saved correctly | ||
// if UPDATE returns "0 rows changed", two possibilities: (a) record doesn't exist (b) value is not changed | ||
// 2. do a SELECT to check if the row exists or not (we already have the transaction lock) | ||
// 3. if the row doesn't exist, do an INSERT (we are still protected by the transaction lock, so it's safe) | ||
// | ||
// to optimize the SELECT in step 2, we can use an extra column like `revision=revision+1` | ||
// to make sure the UPDATE always returns a non-zero value for existing (unchanged) records. | ||
|
||
res, err := e.Exec("UPDATE repo_setting SET setting_value=?, version = version+1 WHERE repo_id=? AND setting_key=? AND version=?", value, repoID, key, version) | ||
if err != nil { | ||
return err | ||
} | ||
rows, _ := res.RowsAffected() | ||
if rows > 0 { | ||
// the existing row is updated, so we can return | ||
return nil | ||
} | ||
|
||
// in case the value isn't changed, update would return 0 rows changed, so we need this check | ||
has, err := e.Exist(&Setting{RepoID: repoID, SettingKey: key}) | ||
if err != nil { | ||
return err | ||
} | ||
if has { | ||
return nil | ||
} | ||
|
||
// if no existing row, insert a new row | ||
_, err = e.Insert(&Setting{RepoID: repoID, SettingKey: key, SettingValue: value}) | ||
return err | ||
}) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
// Copyright 2022 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package repo | ||
lunny marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// Copyright 2022 The Gitea Authors. All rights reserved. | ||
// SPDX-License-Identifier: MIT | ||
|
||
package repo | ||
|
||
import ( | ||
"testing" | ||
|
||
"code.gitea.io/gitea/models/unittest" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestSettings(t *testing.T) { | ||
keyName := "test_repo_setting" | ||
assert.NoError(t, unittest.PrepareTestDatabase()) | ||
|
||
newSetting := &Setting{RepoID: 99, SettingKey: keyName, SettingValue: "Gitea Repo Setting Test"} | ||
|
||
// create setting | ||
err := SetSetting(newSetting) | ||
assert.NoError(t, err) | ||
// test about saving unchanged values | ||
err = SetSetting(newSetting) | ||
assert.NoError(t, err) | ||
|
||
// get specific setting | ||
settings, err := GetSettings(99, []string{keyName}) | ||
assert.NoError(t, err) | ||
assert.Len(t, settings, 1) | ||
assert.EqualValues(t, newSetting.SettingValue, settings[keyName].SettingValue) | ||
|
||
settingValue, err := GetSetting(99, keyName) | ||
assert.NoError(t, err) | ||
assert.EqualValues(t, newSetting.SettingValue, settingValue) | ||
|
||
settingValue, err = GetSetting(99, "no_such") | ||
assert.NoError(t, err) | ||
assert.EqualValues(t, "", settingValue) | ||
|
||
// updated setting | ||
updatedSetting := &Setting{RepoID: 99, SettingKey: keyName, SettingValue: "Updated"} | ||
err = SetSetting(updatedSetting) | ||
assert.NoError(t, err) | ||
|
||
// get all settings | ||
settings, err = GetAllSettings(99) | ||
assert.NoError(t, err) | ||
assert.Len(t, settings, 1) | ||
assert.EqualValues(t, updatedSetting.SettingValue, settings[updatedSetting.SettingKey].SettingValue) | ||
|
||
// delete setting | ||
err = DeleteSetting(99, keyName) | ||
assert.NoError(t, err) | ||
settings, err = GetAllSettings(99) | ||
assert.NoError(t, err) | ||
assert.Len(t, settings, 0) | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.