mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-22 20:05:48 +02:00
Merge remote-tracking branch 'upstream/main' into limit-repo-size.
This commit is contained in:
@@ -3,14 +3,22 @@
|
||||
|
||||
package setting
|
||||
|
||||
import "code.gitea.io/gitea/modules/container"
|
||||
|
||||
// Admin settings
|
||||
var Admin struct {
|
||||
DisableRegularOrgCreation bool
|
||||
DefaultEmailNotification string
|
||||
UserDisabledFeatures container.Set[string]
|
||||
}
|
||||
|
||||
func loadAdminFrom(rootCfg ConfigProvider) {
|
||||
mustMapSetting(rootCfg, "admin", &Admin)
|
||||
sec := rootCfg.Section("admin")
|
||||
Admin.DisableRegularOrgCreation = sec.Key("DISABLE_REGULAR_ORG_CREATION").MustBool(false)
|
||||
Admin.DefaultEmailNotification = sec.Key("DEFAULT_EMAIL_NOTIFICATIONS").MustString("enabled")
|
||||
Admin.UserDisabledFeatures = container.SetOf(sec.Key("USER_DISABLED_FEATURES").Strings(",")...)
|
||||
}
|
||||
|
||||
const (
|
||||
UserFeatureDeletion = "deletion"
|
||||
)
|
||||
|
||||
@@ -26,7 +26,7 @@ func loadAttachmentFrom(rootCfg ConfigProvider) (err error) {
|
||||
}
|
||||
|
||||
Attachment.AllowedTypes = sec.Key("ALLOWED_TYPES").MustString(".csv,.docx,.fodg,.fodp,.fods,.fodt,.gif,.gz,.jpeg,.jpg,.log,.md,.mov,.mp4,.odf,.odg,.odp,.ods,.odt,.patch,.pdf,.png,.pptx,.svg,.tgz,.txt,.webm,.xls,.xlsx,.zip")
|
||||
Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(4)
|
||||
Attachment.MaxSize = sec.Key("MAX_SIZE").MustInt64(2048)
|
||||
Attachment.MaxFiles = sec.Key("MAX_FILES").MustInt(5)
|
||||
Attachment.Enabled = sec.Key("ENABLED").MustBool(true)
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
// Cache represents cache settings
|
||||
type Cache struct {
|
||||
Enabled bool
|
||||
Adapter string
|
||||
Interval int
|
||||
Conn string
|
||||
@@ -24,23 +23,19 @@ var CacheService = struct {
|
||||
Cache `ini:"cache"`
|
||||
|
||||
LastCommit struct {
|
||||
Enabled bool
|
||||
TTL time.Duration `ini:"ITEM_TTL"`
|
||||
CommitsCount int64
|
||||
} `ini:"cache.last_commit"`
|
||||
}{
|
||||
Cache: Cache{
|
||||
Enabled: true,
|
||||
Adapter: "memory",
|
||||
Interval: 60,
|
||||
TTL: 16 * time.Hour,
|
||||
},
|
||||
LastCommit: struct {
|
||||
Enabled bool
|
||||
TTL time.Duration `ini:"ITEM_TTL"`
|
||||
CommitsCount int64
|
||||
}{
|
||||
Enabled: true,
|
||||
TTL: 8760 * time.Hour,
|
||||
CommitsCount: 1000,
|
||||
},
|
||||
@@ -65,30 +60,12 @@ func loadCacheFrom(rootCfg ConfigProvider) {
|
||||
if CacheService.Conn == "" {
|
||||
CacheService.Conn = "50000"
|
||||
}
|
||||
case "": // disable cache
|
||||
CacheService.Enabled = false
|
||||
default:
|
||||
log.Fatal("Unknown cache adapter: %s", CacheService.Adapter)
|
||||
}
|
||||
|
||||
if CacheService.Enabled {
|
||||
log.Info("Cache Service Enabled")
|
||||
} else {
|
||||
log.Warn("Cache Service Disabled so that captcha disabled too")
|
||||
// captcha depends on cache service
|
||||
Service.EnableCaptcha = false
|
||||
}
|
||||
|
||||
sec = rootCfg.Section("cache.last_commit")
|
||||
if !CacheService.Enabled {
|
||||
CacheService.LastCommit.Enabled = false
|
||||
}
|
||||
|
||||
CacheService.LastCommit.CommitsCount = sec.Key("COMMITS_COUNT").MustInt64(1000)
|
||||
|
||||
if CacheService.LastCommit.Enabled {
|
||||
log.Info("Last Commit Cache Service Enabled")
|
||||
}
|
||||
}
|
||||
|
||||
// TTLSeconds returns the TTLSeconds or unix timestamp for memcache
|
||||
|
||||
@@ -15,8 +15,45 @@ type PictureStruct struct {
|
||||
EnableFederatedAvatar *config.Value[bool]
|
||||
}
|
||||
|
||||
type OpenWithEditorApp struct {
|
||||
DisplayName string
|
||||
OpenURL string
|
||||
}
|
||||
|
||||
type OpenWithEditorAppsType []OpenWithEditorApp
|
||||
|
||||
func (t OpenWithEditorAppsType) ToTextareaString() string {
|
||||
ret := ""
|
||||
for _, app := range t {
|
||||
ret += app.DisplayName + " = " + app.OpenURL + "\n"
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func DefaultOpenWithEditorApps() OpenWithEditorAppsType {
|
||||
return OpenWithEditorAppsType{
|
||||
{
|
||||
DisplayName: "VS Code",
|
||||
OpenURL: "vscode://vscode.git/clone?url={url}",
|
||||
},
|
||||
{
|
||||
DisplayName: "VSCodium",
|
||||
OpenURL: "vscodium://vscode.git/clone?url={url}",
|
||||
},
|
||||
{
|
||||
DisplayName: "Intellij IDEA",
|
||||
OpenURL: "jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo={url}",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type RepositoryStruct struct {
|
||||
OpenWithEditorApps *config.Value[OpenWithEditorAppsType]
|
||||
}
|
||||
|
||||
type ConfigStruct struct {
|
||||
Picture *PictureStruct
|
||||
Picture *PictureStruct
|
||||
Repository *RepositoryStruct
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -28,8 +65,11 @@ func initDefaultConfig() {
|
||||
config.SetCfgSecKeyGetter(&cfgSecKeyGetter{})
|
||||
defaultConfig = &ConfigStruct{
|
||||
Picture: &PictureStruct{
|
||||
DisableGravatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}, "picture.disable_gravatar"),
|
||||
EnableFederatedAvatar: config.Bool(false, config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}, "picture.enable_federated_avatar"),
|
||||
DisableGravatar: config.ValueJSON[bool]("picture.disable_gravatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "DISABLE_GRAVATAR"}),
|
||||
EnableFederatedAvatar: config.ValueJSON[bool]("picture.enable_federated_avatar").WithFileConfig(config.CfgSecKey{Sec: "picture", Key: "ENABLE_FEDERATED_AVATAR"}),
|
||||
},
|
||||
Repository: &RepositoryStruct{
|
||||
OpenWithEditorApps: config.ValueJSON[OpenWithEditorAppsType]("repository.open-with.editor-apps"),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -42,6 +82,9 @@ func Config() *ConfigStruct {
|
||||
type cfgSecKeyGetter struct{}
|
||||
|
||||
func (c cfgSecKeyGetter) GetValue(sec, key string) (v string, has bool) {
|
||||
if key == "" {
|
||||
return "", false
|
||||
}
|
||||
cfgSec, err := CfgProvider.GetSection(sec)
|
||||
if err != nil {
|
||||
log.Error("Unable to get config section: %q", sec)
|
||||
|
||||
@@ -5,8 +5,11 @@ package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type CfgSecKey struct {
|
||||
@@ -23,14 +26,14 @@ type Value[T any] struct {
|
||||
revision int
|
||||
}
|
||||
|
||||
func (value *Value[T]) parse(s string) (v T) {
|
||||
switch any(v).(type) {
|
||||
case bool:
|
||||
b, _ := strconv.ParseBool(s)
|
||||
return any(b).(T)
|
||||
default:
|
||||
panic("unsupported config type, please complete the code")
|
||||
func (value *Value[T]) parse(key, valStr string) (v T) {
|
||||
v = value.def
|
||||
if valStr != "" {
|
||||
if err := json.Unmarshal(util.UnsafeStringToBytes(valStr), &v); err != nil {
|
||||
log.Error("Unable to unmarshal json config for key %q, err: %v", key, err)
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (value *Value[T]) Value(ctx context.Context) (v T) {
|
||||
@@ -62,7 +65,7 @@ func (value *Value[T]) Value(ctx context.Context) (v T) {
|
||||
if valStr == nil {
|
||||
v = value.def
|
||||
} else {
|
||||
v = value.parse(*valStr)
|
||||
v = value.parse(value.dynKey, *valStr)
|
||||
}
|
||||
|
||||
value.mu.Lock()
|
||||
@@ -76,6 +79,16 @@ func (value *Value[T]) DynKey() string {
|
||||
return value.dynKey
|
||||
}
|
||||
|
||||
func Bool(def bool, cfgSecKey CfgSecKey, dynKey string) *Value[bool] {
|
||||
return &Value[bool]{def: def, cfgSecKey: cfgSecKey, dynKey: dynKey}
|
||||
func (value *Value[T]) WithDefault(def T) *Value[T] {
|
||||
value.def = def
|
||||
return value
|
||||
}
|
||||
|
||||
func (value *Value[T]) WithFileConfig(cfgSecKey CfgSecKey) *Value[T] {
|
||||
value.cfgSecKey = cfgSecKey
|
||||
return value
|
||||
}
|
||||
|
||||
func ValueJSON[T any](dynKey string) *Value[T] {
|
||||
return &Value[T]{dynKey: dynKey}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
|
||||
|
||||
// NewConfigProviderFromFile load configuration from file.
|
||||
// NOTE: do not print any log except error.
|
||||
func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) {
|
||||
func NewConfigProviderFromFile(file string) (ConfigProvider, error) {
|
||||
cfg := ini.Empty(configProviderLoadOptions())
|
||||
loadedFromEmpty := true
|
||||
|
||||
@@ -213,12 +213,6 @@ func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvi
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range extraConfigs {
|
||||
if err := cfg.Append([]byte(s)); err != nil {
|
||||
return nil, fmt.Errorf("unable to append more config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
cfg.NameMapper = ini.SnackCase
|
||||
return &iniConfigProvider{
|
||||
file: file,
|
||||
|
||||
@@ -12,9 +12,7 @@ import (
|
||||
// CORSConfig defines CORS settings
|
||||
var CORSConfig = struct {
|
||||
Enabled bool
|
||||
Scheme string
|
||||
AllowDomain []string
|
||||
AllowSubdomain bool
|
||||
AllowDomain []string // FIXME: this option is from legacy code, it actually works as "AllowedOrigins". When refactoring in the future, the config option should also be renamed together.
|
||||
Methods []string
|
||||
MaxAge time.Duration
|
||||
AllowCredentials bool
|
||||
|
||||
+28
-26
@@ -9,7 +9,6 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -26,25 +25,27 @@ var (
|
||||
|
||||
// Database holds the database settings
|
||||
Database = struct {
|
||||
Type DatabaseType
|
||||
Host string
|
||||
Name string
|
||||
User string
|
||||
Passwd string
|
||||
Schema string
|
||||
SSLMode string
|
||||
Path string
|
||||
LogSQL bool
|
||||
MysqlCharset string
|
||||
Timeout int // seconds
|
||||
SQLiteJournalMode string
|
||||
DBConnectRetries int
|
||||
DBConnectBackoff time.Duration
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
ConnMaxLifetime time.Duration
|
||||
IterateBufferSize int
|
||||
AutoMigration bool
|
||||
Type DatabaseType
|
||||
Host string
|
||||
Name string
|
||||
User string
|
||||
Passwd string
|
||||
Schema string
|
||||
SSLMode string
|
||||
Path string
|
||||
LogSQL bool
|
||||
MysqlCharset string
|
||||
CharsetCollation string
|
||||
Timeout int // seconds
|
||||
SQLiteJournalMode string
|
||||
DBConnectRetries int
|
||||
DBConnectBackoff time.Duration
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
ConnMaxLifetime time.Duration
|
||||
IterateBufferSize int
|
||||
AutoMigration bool
|
||||
SlowQueryThreshold time.Duration
|
||||
}{
|
||||
Timeout: 500,
|
||||
IterateBufferSize: 50,
|
||||
@@ -68,7 +69,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
|
||||
}
|
||||
Database.Schema = sec.Key("SCHEMA").String()
|
||||
Database.SSLMode = sec.Key("SSL_MODE").MustString("disable")
|
||||
Database.MysqlCharset = sec.Key("MYSQL_CHARSET").MustString("utf8mb4") // do not document it, end users won't need it.
|
||||
Database.CharsetCollation = sec.Key("CHARSET_COLLATION").String()
|
||||
|
||||
Database.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "gitea.db"))
|
||||
Database.Timeout = sec.Key("SQLITE_TIMEOUT").MustInt(500)
|
||||
@@ -87,6 +88,7 @@ func loadDBSetting(rootCfg ConfigProvider) {
|
||||
Database.DBConnectRetries = sec.Key("DB_RETRIES").MustInt(10)
|
||||
Database.DBConnectBackoff = sec.Key("DB_RETRY_BACKOFF").MustDuration(3 * time.Second)
|
||||
Database.AutoMigration = sec.Key("AUTO_MIGRATION").MustBool(true)
|
||||
Database.SlowQueryThreshold = sec.Key("SLOW_QUERY_THRESHOLD").MustDuration(5 * time.Second)
|
||||
}
|
||||
|
||||
// DBConnStr returns database connection string
|
||||
@@ -106,8 +108,8 @@ func DBConnStr() (string, error) {
|
||||
if tls == "disable" { // allow (Postgres-inspired) default value to work in MySQL
|
||||
tls = "false"
|
||||
}
|
||||
connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%scharset=%s&parseTime=true&tls=%s",
|
||||
Database.User, Database.Passwd, connType, Database.Host, Database.Name, paramSep, Database.MysqlCharset, tls)
|
||||
connStr = fmt.Sprintf("%s:%s@%s(%s)/%s%sparseTime=true&tls=%s",
|
||||
Database.User, Database.Passwd, connType, Database.Host, Database.Name, paramSep, tls)
|
||||
case "postgres":
|
||||
connStr = getPostgreSQLConnectionString(Database.Host, Database.User, Database.Passwd, Database.Name, Database.SSLMode)
|
||||
case "mssql":
|
||||
@@ -117,7 +119,7 @@ func DBConnStr() (string, error) {
|
||||
if !EnableSQLite3 {
|
||||
return "", errors.New("this Gitea binary was not built with SQLite3 support")
|
||||
}
|
||||
if err := os.MkdirAll(path.Dir(Database.Path), os.ModePerm); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(Database.Path), os.ModePerm); err != nil {
|
||||
return "", fmt.Errorf("Failed to create directories: %w", err)
|
||||
}
|
||||
journalMode := ""
|
||||
@@ -169,8 +171,8 @@ func getPostgreSQLConnectionString(dbHost, dbUser, dbPasswd, dbName, dbsslMode s
|
||||
RawQuery: dbParam,
|
||||
}
|
||||
query := connURL.Query()
|
||||
if dbHost[0] == '/' { // looks like a unix socket
|
||||
query.Add("host", dbHost)
|
||||
if strings.HasPrefix(host, "/") { // looks like a unix socket
|
||||
query.Add("host", host)
|
||||
connURL.Host = ":" + port
|
||||
}
|
||||
query.Set("sslmode", dbsslMode)
|
||||
|
||||
@@ -65,6 +65,10 @@ func Test_getPostgreSQLConnectionString(t *testing.T) {
|
||||
SSLMode string
|
||||
Output string
|
||||
}{
|
||||
{
|
||||
Host: "", // empty means default
|
||||
Output: "postgres://:@127.0.0.1:5432?sslmode=",
|
||||
},
|
||||
{
|
||||
Host: "/tmp/pg.sock",
|
||||
User: "testuser",
|
||||
@@ -73,6 +77,14 @@ func Test_getPostgreSQLConnectionString(t *testing.T) {
|
||||
SSLMode: "false",
|
||||
Output: "postgres://testuser:space%20space%20%21%23$%25%5E%5E%25%5E%60%60%60-=%3F=@:5432/gitea?host=%2Ftmp%2Fpg.sock&sslmode=false",
|
||||
},
|
||||
{
|
||||
Host: "/tmp/pg.sock:6432",
|
||||
User: "testuser",
|
||||
Passwd: "pass",
|
||||
Name: "gitea",
|
||||
SSLMode: "false",
|
||||
Output: "postgres://testuser:pass@:6432/gitea?host=%2Ftmp%2Fpg.sock&sslmode=false",
|
||||
},
|
||||
{
|
||||
Host: "localhost",
|
||||
User: "pgsqlusername",
|
||||
|
||||
+17
-14
@@ -53,21 +53,24 @@ var Indexer = struct {
|
||||
func loadIndexerFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("indexer")
|
||||
Indexer.IssueType = sec.Key("ISSUE_INDEXER_TYPE").MustString("bleve")
|
||||
Indexer.IssuePath = filepath.ToSlash(sec.Key("ISSUE_INDEXER_PATH").MustString(filepath.ToSlash(filepath.Join(AppDataPath, "indexers/issues.bleve"))))
|
||||
if !filepath.IsAbs(Indexer.IssuePath) {
|
||||
Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
|
||||
}
|
||||
Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
|
||||
|
||||
if Indexer.IssueType == "meilisearch" {
|
||||
u, err := url.Parse(Indexer.IssueConnStr)
|
||||
if err != nil {
|
||||
log.Warn("Failed to parse ISSUE_INDEXER_CONN_STR: %v", err)
|
||||
u = &url.URL{}
|
||||
if Indexer.IssueType == "bleve" {
|
||||
Indexer.IssuePath = filepath.ToSlash(sec.Key("ISSUE_INDEXER_PATH").MustString(filepath.ToSlash(filepath.Join(AppDataPath, "indexers/issues.bleve"))))
|
||||
if !filepath.IsAbs(Indexer.IssuePath) {
|
||||
Indexer.IssuePath = filepath.ToSlash(filepath.Join(AppWorkPath, Indexer.IssuePath))
|
||||
}
|
||||
fatalDuplicatedPath("issue_indexer", Indexer.IssuePath)
|
||||
} else {
|
||||
Indexer.IssueConnStr = sec.Key("ISSUE_INDEXER_CONN_STR").MustString(Indexer.IssueConnStr)
|
||||
if Indexer.IssueType == "meilisearch" {
|
||||
u, err := url.Parse(Indexer.IssueConnStr)
|
||||
if err != nil {
|
||||
log.Warn("Failed to parse ISSUE_INDEXER_CONN_STR: %v", err)
|
||||
u = &url.URL{}
|
||||
}
|
||||
Indexer.IssueConnAuth, _ = u.User.Password()
|
||||
u.User = nil
|
||||
Indexer.IssueConnStr = u.String()
|
||||
}
|
||||
Indexer.IssueConnAuth, _ = u.User.Password()
|
||||
u.User = nil
|
||||
Indexer.IssueConnStr = u.String()
|
||||
}
|
||||
|
||||
Indexer.IssueIndexerName = sec.Key("ISSUE_INDEXER_NAME").MustString(Indexer.IssueIndexerName)
|
||||
|
||||
+10
-13
@@ -4,22 +4,19 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/generate"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// LFS represents the configuration for Git LFS
|
||||
var LFS = struct {
|
||||
StartServer bool `ini:"LFS_START_SERVER"`
|
||||
JWTSecretBase64 string `ini:"LFS_JWT_SECRET"`
|
||||
JWTSecretBytes []byte `ini:"-"`
|
||||
HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
|
||||
MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"`
|
||||
LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"`
|
||||
StartServer bool `ini:"LFS_START_SERVER"`
|
||||
JWTSecretBytes []byte `ini:"-"`
|
||||
HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"`
|
||||
MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"`
|
||||
LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"`
|
||||
|
||||
Storage *Storage
|
||||
}{}
|
||||
@@ -61,10 +58,10 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
LFS.JWTSecretBase64 = loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
||||
LFS.JWTSecretBytes, err = util.Base64FixedDecode(base64.RawURLEncoding, []byte(LFS.JWTSecretBase64), 32)
|
||||
jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
||||
LFS.JWTSecretBytes, err = generate.DecodeJwtSecretBase64(jwtSecretBase64)
|
||||
if err != nil {
|
||||
LFS.JWTSecretBytes, LFS.JWTSecretBase64, err = generate.NewJwtSecretBase64()
|
||||
LFS.JWTSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error generating JWT Secret for custom config: %v", err)
|
||||
}
|
||||
@@ -74,8 +71,8 @@ func loadLFSFrom(rootCfg ConfigProvider) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
|
||||
}
|
||||
rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
|
||||
saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(LFS.JWTSecretBase64)
|
||||
rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
if err := saveCfg.Save(); err != nil {
|
||||
return fmt.Errorf("error saving JWT Secret for custom config: %v", err)
|
||||
}
|
||||
|
||||
+42
-16
@@ -4,13 +4,12 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
|
||||
"code.gitea.io/gitea/modules/generate"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// OAuth2UsernameType is enum describing the way gitea 'name' should be generated from oauth2 data
|
||||
@@ -21,7 +20,7 @@ const (
|
||||
OAuth2UsernameUserid OAuth2UsernameType = "userid"
|
||||
// OAuth2UsernameNickname oauth2 nickname field will be used as gitea name
|
||||
OAuth2UsernameNickname OAuth2UsernameType = "nickname"
|
||||
// OAuth2UsernameEmail username of oauth2 email filed will be used as gitea name
|
||||
// OAuth2UsernameEmail username of oauth2 email field will be used as gitea name
|
||||
OAuth2UsernameEmail OAuth2UsernameType = "email"
|
||||
)
|
||||
|
||||
@@ -93,17 +92,16 @@ func parseScopes(sec ConfigSection, name string) []string {
|
||||
}
|
||||
|
||||
var OAuth2 = struct {
|
||||
Enable bool
|
||||
Enabled bool
|
||||
AccessTokenExpirationTime int64
|
||||
RefreshTokenExpirationTime int64
|
||||
InvalidateRefreshTokens bool
|
||||
JWTSigningAlgorithm string `ini:"JWT_SIGNING_ALGORITHM"`
|
||||
JWTSecretBase64 string `ini:"JWT_SECRET"`
|
||||
JWTSigningPrivateKeyFile string `ini:"JWT_SIGNING_PRIVATE_KEY_FILE"`
|
||||
MaxTokenLength int
|
||||
DefaultApplications []string
|
||||
}{
|
||||
Enable: true,
|
||||
Enabled: true,
|
||||
AccessTokenExpirationTime: 3600,
|
||||
RefreshTokenExpirationTime: 730,
|
||||
InvalidateRefreshTokens: false,
|
||||
@@ -114,38 +112,66 @@ var OAuth2 = struct {
|
||||
}
|
||||
|
||||
func loadOAuth2From(rootCfg ConfigProvider) {
|
||||
if err := rootCfg.Section("oauth2").MapTo(&OAuth2); err != nil {
|
||||
log.Fatal("Failed to OAuth2 settings: %v", err)
|
||||
sec := rootCfg.Section("oauth2")
|
||||
if err := sec.MapTo(&OAuth2); err != nil {
|
||||
log.Fatal("Failed to map OAuth2 settings: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !OAuth2.Enable {
|
||||
// Handle the rename of ENABLE to ENABLED
|
||||
deprecatedSetting(rootCfg, "oauth2", "ENABLE", "oauth2", "ENABLED", "v1.23.0")
|
||||
if sec.HasKey("ENABLE") && !sec.HasKey("ENABLED") {
|
||||
OAuth2.Enabled = sec.Key("ENABLE").MustBool(OAuth2.Enabled)
|
||||
}
|
||||
|
||||
if !OAuth2.Enabled {
|
||||
return
|
||||
}
|
||||
|
||||
OAuth2.JWTSecretBase64 = loadSecret(rootCfg.Section("oauth2"), "JWT_SECRET_URI", "JWT_SECRET")
|
||||
jwtSecretBase64 := loadSecret(sec, "JWT_SECRET_URI", "JWT_SECRET")
|
||||
|
||||
if !filepath.IsAbs(OAuth2.JWTSigningPrivateKeyFile) {
|
||||
OAuth2.JWTSigningPrivateKeyFile = filepath.Join(AppDataPath, OAuth2.JWTSigningPrivateKeyFile)
|
||||
}
|
||||
|
||||
if InstallLock {
|
||||
if _, err := util.Base64FixedDecode(base64.RawURLEncoding, []byte(OAuth2.JWTSecretBase64), 32); err != nil {
|
||||
key, err := generate.NewJwtSecret()
|
||||
jwtSecretBytes, err := generate.DecodeJwtSecretBase64(jwtSecretBase64)
|
||||
if err != nil {
|
||||
jwtSecretBytes, jwtSecretBase64, err = generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
log.Fatal("error generating JWT secret: %v", err)
|
||||
}
|
||||
|
||||
OAuth2.JWTSecretBase64 = base64.RawURLEncoding.EncodeToString(key)
|
||||
saveCfg, err := rootCfg.PrepareSaving()
|
||||
if err != nil {
|
||||
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
|
||||
}
|
||||
rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
|
||||
saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(OAuth2.JWTSecretBase64)
|
||||
rootCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
saveCfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
|
||||
if err := saveCfg.Save(); err != nil {
|
||||
log.Fatal("save oauth2.JWT_SECRET failed: %v", err)
|
||||
}
|
||||
}
|
||||
generalSigningSecret.Store(&jwtSecretBytes)
|
||||
}
|
||||
}
|
||||
|
||||
// generalSigningSecret is used as container for a []byte value
|
||||
// instead of an additional mutex, we use CompareAndSwap func to change the value thread save
|
||||
var generalSigningSecret atomic.Pointer[[]byte]
|
||||
|
||||
func GetGeneralTokenSigningSecret() []byte {
|
||||
old := generalSigningSecret.Load()
|
||||
if old == nil || len(*old) == 0 {
|
||||
jwtSecret, _, err := generate.NewJwtSecretWithBase64()
|
||||
if err != nil {
|
||||
log.Fatal("Unable to generate general JWT secret: %s", err.Error())
|
||||
}
|
||||
if generalSigningSecret.CompareAndSwap(old, &jwtSecret) {
|
||||
// FIXME: in main branch, the signing token should be refactored (eg: one unique for LFS/OAuth2/etc ...)
|
||||
log.Warn("OAuth2 is not enabled, unable to use a persistent signing secret, a new one is generated, which is not persistent between restarts and cluster nodes")
|
||||
return jwtSecret
|
||||
}
|
||||
return *generalSigningSecret.Load()
|
||||
}
|
||||
return *old
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package setting
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/generate"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetGeneralSigningSecret(t *testing.T) {
|
||||
// when there is no general signing secret, it should be generated, and keep the same value
|
||||
assert.Nil(t, generalSigningSecret.Load())
|
||||
s1 := GetGeneralTokenSigningSecret()
|
||||
assert.NotNil(t, s1)
|
||||
s2 := GetGeneralTokenSigningSecret()
|
||||
assert.Equal(t, s1, s2)
|
||||
|
||||
// the config value should always override any pre-generated value
|
||||
cfg, _ := NewConfigProviderFromData(`
|
||||
[oauth2]
|
||||
JWT_SECRET = BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||
`)
|
||||
defer test.MockVariableValue(&InstallLock, true)()
|
||||
loadOAuth2From(cfg)
|
||||
actual := GetGeneralTokenSigningSecret()
|
||||
expected, _ := generate.DecodeJwtSecretBase64("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")
|
||||
assert.Len(t, actual, 32)
|
||||
assert.EqualValues(t, expected, actual)
|
||||
}
|
||||
@@ -66,8 +66,12 @@ func init() {
|
||||
AppWorkPath = filepath.Dir(AppPath)
|
||||
}
|
||||
|
||||
fatalDuplicatedPath("app_work_path", AppWorkPath)
|
||||
|
||||
appWorkPathBuiltin = AppWorkPath
|
||||
customPathBuiltin = CustomPath
|
||||
|
||||
fatalDuplicatedPath("custom_path", CustomPath)
|
||||
customConfBuiltin = CustomConf
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ var (
|
||||
PopulateSquashCommentWithCommitMessages bool
|
||||
AddCoCommitterTrailers bool
|
||||
TestConflictingPatchesWithGitApply bool
|
||||
RetargetChildrenOnMerge bool
|
||||
} `ini:"repository.pull-request"`
|
||||
|
||||
// Issue Setting
|
||||
@@ -211,6 +212,7 @@ var (
|
||||
PopulateSquashCommentWithCommitMessages bool
|
||||
AddCoCommitterTrailers bool
|
||||
TestConflictingPatchesWithGitApply bool
|
||||
RetargetChildrenOnMerge bool
|
||||
}{
|
||||
WorkInProgressPrefixes: []string{"WIP:", "[WIP]"},
|
||||
// Same as GitHub. See
|
||||
@@ -225,6 +227,7 @@ var (
|
||||
DefaultMergeMessageOfficialApproversOnly: true,
|
||||
PopulateSquashCommentWithCommitMessages: false,
|
||||
AddCoCommitterTrailers: true,
|
||||
RetargetChildrenOnMerge: true,
|
||||
},
|
||||
|
||||
// Issue settings
|
||||
@@ -307,6 +310,9 @@ func loadRepositoryFrom(rootCfg ConfigProvider) {
|
||||
} else {
|
||||
RepoRootPath = filepath.Clean(RepoRootPath)
|
||||
}
|
||||
|
||||
fatalDuplicatedPath("repository.ROOT", RepoRootPath)
|
||||
|
||||
defaultDetectedCharsetsOrder := make([]string, 0, len(Repository.DetectedCharsetsOrder))
|
||||
for _, charset := range Repository.DetectedCharsetsOrder {
|
||||
defaultDetectedCharsetsOrder = append(defaultDetectedCharsetsOrder, strings.ToLower(strings.TrimSpace(charset)))
|
||||
|
||||
@@ -34,6 +34,7 @@ var (
|
||||
PasswordHashAlgo string
|
||||
PasswordCheckPwn bool
|
||||
SuccessfulTokensCacheSize int
|
||||
DisableQueryAuthToken bool
|
||||
CSRFCookieName = "_csrf"
|
||||
CSRFCookieHTTPOnly = true
|
||||
)
|
||||
@@ -157,4 +158,14 @@ func loadSecurityFrom(rootCfg ConfigProvider) {
|
||||
PasswordComplexity = append(PasswordComplexity, name)
|
||||
}
|
||||
}
|
||||
|
||||
sectionHasDisableQueryAuthToken := sec.HasKey("DISABLE_QUERY_AUTH_TOKEN")
|
||||
|
||||
// TODO: default value should be true in future releases
|
||||
DisableQueryAuthToken = sec.Key("DISABLE_QUERY_AUTH_TOKEN").MustBool(false)
|
||||
|
||||
// warn if the setting is set to false explicitly
|
||||
if sectionHasDisableQueryAuthToken && !DisableQueryAuthToken {
|
||||
log.Warn("Enabling Query API Auth tokens is not recommended. DISABLE_QUERY_AUTH_TOKEN will default to true in gitea 1.23 and will be removed in gitea 1.24.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -315,23 +314,25 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
||||
RedirectOtherPort = sec.Key("REDIRECT_OTHER_PORT").MustBool(false)
|
||||
PortToRedirect = sec.Key("PORT_TO_REDIRECT").MustString("80")
|
||||
RedirectorUseProxyProtocol = sec.Key("REDIRECTOR_USE_PROXY_PROTOCOL").MustBool(UseProxyProtocol)
|
||||
OfflineMode = sec.Key("OFFLINE_MODE").MustBool()
|
||||
OfflineMode = sec.Key("OFFLINE_MODE").MustBool(true)
|
||||
if len(StaticRootPath) == 0 {
|
||||
StaticRootPath = AppWorkPath
|
||||
}
|
||||
StaticRootPath = sec.Key("STATIC_ROOT_PATH").MustString(StaticRootPath)
|
||||
StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour)
|
||||
AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data"))
|
||||
AppDataPath = sec.Key("APP_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data"))
|
||||
if !filepath.IsAbs(AppDataPath) {
|
||||
AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath))
|
||||
}
|
||||
fatalDuplicatedPath("app_data_path", AppDataPath)
|
||||
|
||||
EnableGzip = sec.Key("ENABLE_GZIP").MustBool()
|
||||
EnablePprof = sec.Key("ENABLE_PPROF").MustBool(false)
|
||||
PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(path.Join(AppWorkPath, "data/tmp/pprof"))
|
||||
PprofDataPath = sec.Key("PPROF_DATA_PATH").MustString(filepath.Join(AppWorkPath, "data/tmp/pprof"))
|
||||
if !filepath.IsAbs(PprofDataPath) {
|
||||
PprofDataPath = filepath.Join(AppWorkPath, PprofDataPath)
|
||||
}
|
||||
fatalDuplicatedPath("pprof_data_path", PprofDataPath)
|
||||
|
||||
landingPage := sec.Key("LANDING_PAGE").MustString("home")
|
||||
switch landingPage {
|
||||
@@ -341,8 +342,7 @@ func loadServerFrom(rootCfg ConfigProvider) {
|
||||
LandingPageURL = LandingPageOrganizations
|
||||
case "login":
|
||||
LandingPageURL = LandingPageLogin
|
||||
case "":
|
||||
case "home":
|
||||
case "", "home":
|
||||
LandingPageURL = LandingPageHome
|
||||
default:
|
||||
LandingPageURL = LandingPage(landingPage)
|
||||
|
||||
@@ -5,7 +5,6 @@ package setting
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
@@ -44,9 +43,10 @@ func loadSessionFrom(rootCfg ConfigProvider) {
|
||||
sec := rootCfg.Section("session")
|
||||
SessionConfig.Provider = sec.Key("PROVIDER").In("memory",
|
||||
[]string{"memory", "file", "redis", "mysql", "postgres", "couchbase", "memcache", "db"})
|
||||
SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(path.Join(AppDataPath, "sessions")), "\" ")
|
||||
SessionConfig.ProviderConfig = strings.Trim(sec.Key("PROVIDER_CONFIG").MustString(filepath.Join(AppDataPath, "sessions")), "\" ")
|
||||
if SessionConfig.Provider == "file" && !filepath.IsAbs(SessionConfig.ProviderConfig) {
|
||||
SessionConfig.ProviderConfig = path.Join(AppWorkPath, SessionConfig.ProviderConfig)
|
||||
SessionConfig.ProviderConfig = filepath.Join(AppWorkPath, SessionConfig.ProviderConfig)
|
||||
fatalDuplicatedPath("session", SessionConfig.ProviderConfig)
|
||||
}
|
||||
SessionConfig.CookieName = sec.Key("COOKIE_NAME").MustString("i_like_gitea")
|
||||
SessionConfig.CookiePath = AppSubURL + "/" // there was a bug, old code only set CookePath=AppSubURL, no trailing slash
|
||||
|
||||
@@ -90,9 +90,9 @@ func PrepareAppDataPath() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitCfgProvider(file string, extraConfigs ...string) {
|
||||
func InitCfgProvider(file string) {
|
||||
var err error
|
||||
if CfgProvider, err = NewConfigProviderFromFile(file, extraConfigs...); err != nil {
|
||||
if CfgProvider, err = NewConfigProviderFromFile(file); err != nil {
|
||||
log.Fatal("Unable to init config provider from %q: %v", file, err)
|
||||
}
|
||||
CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls
|
||||
@@ -226,3 +226,12 @@ func LoadSettingsForInstall() {
|
||||
loadServiceFrom(CfgProvider)
|
||||
loadMailerFrom(CfgProvider)
|
||||
}
|
||||
|
||||
var uniquePaths = make(map[string]string)
|
||||
|
||||
func fatalDuplicatedPath(name, p string) {
|
||||
if targetName, ok := uniquePaths[p]; ok && targetName != name {
|
||||
log.Fatal("storage path %q is being used by %q and %q and all storage paths must be unique to prevent data loss.", p, targetName, name)
|
||||
}
|
||||
uniquePaths[p] = name
|
||||
}
|
||||
|
||||
@@ -240,6 +240,8 @@ func getStorageForLocal(targetSec, overrideSec ConfigSection, tp targetSecType,
|
||||
}
|
||||
}
|
||||
|
||||
fatalDuplicatedPath("storage."+name, storage.Path)
|
||||
|
||||
return &storage, nil
|
||||
}
|
||||
|
||||
|
||||
+52
-40
@@ -7,33 +7,37 @@ import (
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// UI settings
|
||||
var UI = struct {
|
||||
ExplorePagingNum int
|
||||
SitemapPagingNum int
|
||||
IssuePagingNum int
|
||||
RepoSearchPagingNum int
|
||||
MembersPagingNum int
|
||||
FeedMaxCommitNum int
|
||||
FeedPagingNum int
|
||||
PackagesPagingNum int
|
||||
GraphMaxCommitNum int
|
||||
CodeCommentLines int
|
||||
ReactionMaxUserNum int
|
||||
MaxDisplayFileSize int64
|
||||
ShowUserEmail bool
|
||||
DefaultShowFullName bool
|
||||
DefaultTheme string
|
||||
Themes []string
|
||||
Reactions []string
|
||||
ReactionsLookup container.Set[string] `ini:"-"`
|
||||
CustomEmojis []string
|
||||
CustomEmojisMap map[string]string `ini:"-"`
|
||||
SearchRepoDescription bool
|
||||
OnlyShowRelevantRepos bool
|
||||
ExploreDefaultSort string `ini:"EXPLORE_PAGING_DEFAULT_SORT"`
|
||||
ExplorePagingNum int
|
||||
SitemapPagingNum int
|
||||
IssuePagingNum int
|
||||
RepoSearchPagingNum int
|
||||
MembersPagingNum int
|
||||
FeedMaxCommitNum int
|
||||
FeedPagingNum int
|
||||
PackagesPagingNum int
|
||||
GraphMaxCommitNum int
|
||||
CodeCommentLines int
|
||||
ReactionMaxUserNum int
|
||||
MaxDisplayFileSize int64
|
||||
ShowUserEmail bool
|
||||
DefaultShowFullName bool
|
||||
DefaultTheme string
|
||||
Themes []string
|
||||
Reactions []string
|
||||
ReactionsLookup container.Set[string] `ini:"-"`
|
||||
CustomEmojis []string
|
||||
CustomEmojisMap map[string]string `ini:"-"`
|
||||
SearchRepoDescription bool
|
||||
OnlyShowRelevantRepos bool
|
||||
ExploreDefaultSort string `ini:"EXPLORE_PAGING_DEFAULT_SORT"`
|
||||
PreferredTimestampTense string
|
||||
|
||||
AmbiguousUnicodeDetection bool
|
||||
|
||||
Notification struct {
|
||||
MinTimeout time.Duration
|
||||
@@ -65,23 +69,27 @@ var UI = struct {
|
||||
Keywords string
|
||||
} `ini:"ui.meta"`
|
||||
}{
|
||||
ExplorePagingNum: 20,
|
||||
SitemapPagingNum: 20,
|
||||
IssuePagingNum: 20,
|
||||
RepoSearchPagingNum: 20,
|
||||
MembersPagingNum: 20,
|
||||
FeedMaxCommitNum: 5,
|
||||
FeedPagingNum: 20,
|
||||
PackagesPagingNum: 20,
|
||||
GraphMaxCommitNum: 100,
|
||||
CodeCommentLines: 4,
|
||||
ReactionMaxUserNum: 10,
|
||||
MaxDisplayFileSize: 8388608,
|
||||
DefaultTheme: `gitea-auto`,
|
||||
Themes: []string{`gitea-auto`, `gitea-light`, `gitea-dark`},
|
||||
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
|
||||
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},
|
||||
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},
|
||||
ExplorePagingNum: 20,
|
||||
SitemapPagingNum: 20,
|
||||
IssuePagingNum: 20,
|
||||
RepoSearchPagingNum: 20,
|
||||
MembersPagingNum: 20,
|
||||
FeedMaxCommitNum: 5,
|
||||
FeedPagingNum: 20,
|
||||
PackagesPagingNum: 20,
|
||||
GraphMaxCommitNum: 100,
|
||||
CodeCommentLines: 4,
|
||||
ReactionMaxUserNum: 10,
|
||||
MaxDisplayFileSize: 8388608,
|
||||
DefaultTheme: `gitea-auto`,
|
||||
Themes: []string{`gitea-auto`, `gitea-light`, `gitea-dark`},
|
||||
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
|
||||
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},
|
||||
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},
|
||||
PreferredTimestampTense: "mixed",
|
||||
|
||||
AmbiguousUnicodeDetection: true,
|
||||
|
||||
Notification: struct {
|
||||
MinTimeout time.Duration
|
||||
TimeoutStep time.Duration
|
||||
@@ -137,6 +145,10 @@ func loadUIFrom(rootCfg ConfigProvider) {
|
||||
UI.DefaultShowFullName = sec.Key("DEFAULT_SHOW_FULL_NAME").MustBool(false)
|
||||
UI.SearchRepoDescription = sec.Key("SEARCH_REPO_DESCRIPTION").MustBool(true)
|
||||
|
||||
if UI.PreferredTimestampTense != "mixed" && UI.PreferredTimestampTense != "absolute" {
|
||||
log.Fatal("ui.PREFERRED_TIMESTAMP_TENSE must be either 'mixed' or 'absolute'")
|
||||
}
|
||||
|
||||
// OnlyShowRelevantRepos=false is important for many private/enterprise instances,
|
||||
// because many private repositories do not have "description/topic", users just want to search by their names.
|
||||
UI.OnlyShowRelevantRepos = sec.Key("ONLY_SHOW_RELEVANT_REPOS").MustBool(false)
|
||||
|
||||
Reference in New Issue
Block a user