mirror of
https://github.com/go-gitea/gitea.git
synced 2026-09-23 06:48:39 +02:00
Merge remote-tracking branch 'upstream/main' into limit-repo-size
Signed-off-by: DmitryFrolovTri <23313323+DmitryFrolovTri@users.noreply.github.com>
This commit is contained in:
@@ -111,28 +111,36 @@ func RequireRepoReaderOr(unitTypes ...unit.Type) func(ctx *Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRepoScopedToken check whether personal access token has repo scope
|
||||
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository) {
|
||||
// CheckRepoScopedToken check whether personal access token has repo scope
|
||||
func CheckRepoScopedToken(ctx *Context, repo *repo_model.Repository, level auth_model.AccessTokenScopeLevel) {
|
||||
if !ctx.IsBasicAuth || ctx.Data["IsApiToken"] != true {
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
scope, ok := ctx.Data["ApiTokenScope"].(auth_model.AccessTokenScope)
|
||||
if ok { // it's a personal access token but not oauth2 token
|
||||
var scopeMatched bool
|
||||
scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopeRepo)
|
||||
|
||||
requiredScopes := auth_model.GetRequiredScopes(level, auth_model.AccessTokenScopeCategoryRepository)
|
||||
|
||||
// check if scope only applies to public resources
|
||||
publicOnly, err := scope.PublicOnly()
|
||||
if err != nil {
|
||||
ctx.ServerError("HasScope", err)
|
||||
return
|
||||
}
|
||||
if !scopeMatched && !repo.IsPrivate {
|
||||
scopeMatched, err = scope.HasScope(auth_model.AccessTokenScopePublicRepo)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasScope", err)
|
||||
return
|
||||
}
|
||||
|
||||
if publicOnly && repo.IsPrivate {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
scopeMatched, err = scope.HasScope(requiredScopes...)
|
||||
if err != nil {
|
||||
ctx.ServerError("HasScope", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !scopeMatched {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
|
||||
@@ -201,23 +201,6 @@ func InitFull(ctx context.Context) (err error) {
|
||||
return syncGitConfig()
|
||||
}
|
||||
|
||||
func enableReflogs() error {
|
||||
if err := configSet("core.logAllRefUpdates", "true"); err != nil {
|
||||
return err
|
||||
}
|
||||
err := configSet("gc.reflogExpire", fmt.Sprintf("%d", setting.Git.Reflog.Expiration))
|
||||
return err
|
||||
}
|
||||
|
||||
func disableReflogs() error {
|
||||
if err := configUnsetAll("core.logAllRefUpdates", "true"); err != nil {
|
||||
return err
|
||||
} else if err := configUnsetAll("gc.reflogExpire", ""); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// syncGitConfig only modifies gitconfig, won't change global variables (otherwise there will be data-race problem)
|
||||
func syncGitConfig() (err error) {
|
||||
if err = os.MkdirAll(HomeDir(), os.ModePerm); err != nil {
|
||||
@@ -249,16 +232,6 @@ func syncGitConfig() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
if setting.Git.Reflog.Enabled {
|
||||
if err := enableReflogs(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := disableReflogs(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if CheckGitVersionAtLeast("2.10") == nil {
|
||||
if err := configSet("receive.advertisePushOptions", "true"); err != nil {
|
||||
return err
|
||||
|
||||
@@ -53,6 +53,11 @@ func (ref *Reference) Commit() (*Commit, error) {
|
||||
return ref.repo.getCommit(ref.Object)
|
||||
}
|
||||
|
||||
// ShortName returns the short name of the reference
|
||||
func (ref *Reference) ShortName() string {
|
||||
return RefName(ref.Name).ShortName()
|
||||
}
|
||||
|
||||
// RefGroup returns the group type of the reference
|
||||
func (ref *Reference) RefGroup() string {
|
||||
return RefName(ref.Name).RefGroup()
|
||||
|
||||
@@ -68,18 +68,16 @@ func (b *EventWriterBaseImpl) Run(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
if b.GetPauseChan != nil {
|
||||
pause := b.GetPauseChan()
|
||||
if pause != nil {
|
||||
select {
|
||||
case <-pause:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
handlePaused := func() {
|
||||
if pause := b.GetPauseChan(); pause != nil {
|
||||
select {
|
||||
case <-pause:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
@@ -88,6 +86,8 @@ func (b *EventWriterBaseImpl) Run(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
handlePaused()
|
||||
|
||||
if exprRegexp != nil {
|
||||
fileLineCaller := fmt.Sprintf("%s:%d:%s", event.Origin.Filename, event.Origin.Line, event.Origin.Caller)
|
||||
matched := exprRegexp.Match([]byte(fileLineCaller)) || exprRegexp.Match([]byte(event.Origin.MsgSimpleText))
|
||||
@@ -149,7 +149,7 @@ func eventWriterStartGo(ctx context.Context, w EventWriter, shared bool) {
|
||||
if shared {
|
||||
ctxDesc = "Logger: EventWriter (shared): " + w.GetWriterName()
|
||||
}
|
||||
writerCtx, writerCancel := newContext(ctx, ctxDesc)
|
||||
writerCtx, writerCancel := newProcessTypedContext(ctx, ctxDesc)
|
||||
go func() {
|
||||
defer writerCancel()
|
||||
defer close(w.Base().stopped)
|
||||
|
||||
+8
-16
@@ -7,16 +7,12 @@ import (
|
||||
"context"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"code.gitea.io/gitea/modules/process"
|
||||
"code.gitea.io/gitea/modules/util/rotatingfilewriter"
|
||||
)
|
||||
|
||||
var (
|
||||
projectPackagePrefix string
|
||||
processTraceDisabled atomic.Int64
|
||||
)
|
||||
var projectPackagePrefix string
|
||||
|
||||
func init() {
|
||||
_, filename, _, _ := runtime.Caller(0)
|
||||
@@ -28,25 +24,21 @@ func init() {
|
||||
|
||||
rotatingfilewriter.ErrorPrintf = FallbackErrorf
|
||||
|
||||
process.Trace = func(start bool, pid process.IDType, description string, parentPID process.IDType, typ string) {
|
||||
// the logger manager has its own mutex lock, so it's safe to use "Load" here
|
||||
if processTraceDisabled.Load() != 0 {
|
||||
return
|
||||
}
|
||||
process.TraceCallback = func(skip int, start bool, pid process.IDType, description string, parentPID process.IDType, typ string) {
|
||||
if start && parentPID != "" {
|
||||
Log(1, TRACE, "Start %s: %s (from %s) (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(parentPID, FgYellow), NewColoredValue(typ, Reset))
|
||||
Log(skip+1, TRACE, "Start %s: %s (from %s) (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(parentPID, FgYellow), NewColoredValue(typ, Reset))
|
||||
} else if start {
|
||||
Log(1, TRACE, "Start %s: %s (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(typ, Reset))
|
||||
Log(skip+1, TRACE, "Start %s: %s (%s)", NewColoredValue(pid, FgHiYellow), description, NewColoredValue(typ, Reset))
|
||||
} else {
|
||||
Log(1, TRACE, "Done %s: %s", NewColoredValue(pid, FgHiYellow), NewColoredValue(description, Reset))
|
||||
Log(skip+1, TRACE, "Done %s: %s", NewColoredValue(pid, FgHiYellow), NewColoredValue(description, Reset))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newContext(parent context.Context, desc string) (ctx context.Context, cancel context.CancelFunc) {
|
||||
func newProcessTypedContext(parent context.Context, desc string) (ctx context.Context, cancel context.CancelFunc) {
|
||||
// the "process manager" also calls "log.Trace()" to output logs, so if we want to create new contexts by the manager, we need to disable the trace temporarily
|
||||
processTraceDisabled.Add(1)
|
||||
defer processTraceDisabled.Add(-1)
|
||||
process.TraceLogDisable(true)
|
||||
defer process.TraceLogDisable(false)
|
||||
ctx, _, cancel = process.GetManager().AddTypedContext(parent, desc, process.SystemProcessType, false)
|
||||
return ctx, cancel
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ func (l *LoggerImpl) GetLevel() Level {
|
||||
|
||||
func NewLoggerWithWriters(ctx context.Context, name string, writer ...EventWriter) *LoggerImpl {
|
||||
l := &LoggerImpl{}
|
||||
l.ctx, l.ctxCancel = newContext(ctx, "Logger: "+name)
|
||||
l.ctx, l.ctxCancel = newProcessTypedContext(ctx, "Logger: "+name)
|
||||
l.LevelLogger = BaseLoggerToGeneralLogger(l)
|
||||
l.eventWriters = map[string]EventWriter{}
|
||||
l.syncLevelInternal()
|
||||
|
||||
@@ -137,6 +137,6 @@ func GetManager() *LoggerManager {
|
||||
|
||||
func NewManager() *LoggerManager {
|
||||
m := &LoggerManager{writers: map[string]EventWriter{}, loggers: map[string]*LoggerImpl{}}
|
||||
m.ctx, m.ctxCancel = newContext(context.Background(), "LoggerManager")
|
||||
m.ctx, m.ctxCancel = newProcessTypedContext(context.Background(), "LoggerManager")
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ const namespace = "gitea_"
|
||||
// exposes gitea metrics for prometheus
|
||||
type Collector struct {
|
||||
Accesses *prometheus.Desc
|
||||
Actions *prometheus.Desc
|
||||
Attachments *prometheus.Desc
|
||||
BuildInfo *prometheus.Desc
|
||||
Comments *prometheus.Desc
|
||||
@@ -56,11 +55,6 @@ func NewCollector() Collector {
|
||||
"Number of Accesses",
|
||||
nil, nil,
|
||||
),
|
||||
Actions: prometheus.NewDesc(
|
||||
namespace+"actions",
|
||||
"Number of Actions",
|
||||
nil, nil,
|
||||
),
|
||||
Attachments: prometheus.NewDesc(
|
||||
namespace+"attachments",
|
||||
"Number of Attachments",
|
||||
@@ -207,7 +201,6 @@ func NewCollector() Collector {
|
||||
// Describe returns all possible prometheus.Desc
|
||||
func (c Collector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- c.Accesses
|
||||
ch <- c.Actions
|
||||
ch <- c.Attachments
|
||||
ch <- c.BuildInfo
|
||||
ch <- c.Comments
|
||||
@@ -246,11 +239,6 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
|
||||
prometheus.GaugeValue,
|
||||
float64(stats.Counter.Access),
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.Actions,
|
||||
prometheus.GaugeValue,
|
||||
float64(stats.Counter.Action),
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.Attachments,
|
||||
prometheus.GaugeValue,
|
||||
|
||||
@@ -28,7 +28,7 @@ type Notifier interface {
|
||||
NotifyDeleteIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue)
|
||||
NotifyIssueChangeMilestone(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldMilestoneID int64)
|
||||
NotifyIssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment)
|
||||
NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment)
|
||||
NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment)
|
||||
NotifyIssueChangeContent(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldContent string)
|
||||
NotifyIssueClearLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue)
|
||||
NotifyIssueChangeTitle(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldTitle string)
|
||||
|
||||
@@ -120,8 +120,8 @@ func (*NullNotifier) NotifyIssueChangeContent(ctx context.Context, doer *user_mo
|
||||
func (*NullNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, assignee *user_model.User, removed bool, comment *issues_model.Comment) {
|
||||
}
|
||||
|
||||
// NotifyPullReviewRequest places a place holder function
|
||||
func (*NullNotifier) NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
|
||||
// NotifyPullRequestReviewRequest places a place holder function
|
||||
func (*NullNotifier) NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
|
||||
}
|
||||
|
||||
// NotifyIssueClearLabels places a place holder function
|
||||
|
||||
@@ -123,7 +123,7 @@ func (m *mailNotifier) NotifyIssueChangeAssignee(ctx context.Context, doer *user
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mailNotifier) NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
|
||||
func (m *mailNotifier) NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
|
||||
if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotifications() != user_model.EmailNotificationsDisabled {
|
||||
ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL())
|
||||
if err := mailer.SendIssueAssignedMail(ctx, issue, doer, ct, comment, []*user_model.User{reviewer}); err != nil {
|
||||
|
||||
@@ -230,10 +230,10 @@ func NotifyIssueChangeAssignee(ctx context.Context, doer *user_model.User, issue
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyPullReviewRequest notifies Request Review change
|
||||
func NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
|
||||
// NotifyPullRequestReviewRequest notifies Request Review change
|
||||
func NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
|
||||
for _, notifier := range notifiers {
|
||||
notifier.NotifyPullReviewRequest(ctx, doer, issue, reviewer, isRequest, comment)
|
||||
notifier.NotifyPullRequestReviewRequest(ctx, doer, issue, reviewer, isRequest, comment)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -229,7 +229,7 @@ func (ns *notificationService) NotifyIssueChangeAssignee(ctx context.Context, do
|
||||
}
|
||||
}
|
||||
|
||||
func (ns *notificationService) NotifyPullReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
|
||||
func (ns *notificationService) NotifyPullRequestReviewRequest(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, reviewer *user_model.User, isRequest bool, comment *issues_model.Comment) {
|
||||
if isRequest {
|
||||
opts := issueNotificationOpts{
|
||||
IssueID: issue.ID,
|
||||
|
||||
@@ -6,10 +6,10 @@ package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"runtime/pprof"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -44,18 +44,35 @@ type IDType string
|
||||
// - it is simply an alias for context.CancelFunc and is only for documentary purposes
|
||||
type FinishedFunc = context.CancelFunc
|
||||
|
||||
var Trace = defaultTrace // this global can be overridden by particular logging packages - thus avoiding import cycles
|
||||
var (
|
||||
traceDisabled atomic.Int64
|
||||
TraceCallback = defaultTraceCallback // this global can be overridden by particular logging packages - thus avoiding import cycles
|
||||
)
|
||||
|
||||
func defaultTrace(start bool, pid IDType, description string, parentPID IDType, typ string) {
|
||||
if start && parentPID != "" {
|
||||
log.Printf("start process %s: %s (from %s) (%s)", pid, description, parentPID, typ)
|
||||
} else if start {
|
||||
log.Printf("start process %s: %s (%s)", pid, description, typ)
|
||||
// defaultTraceCallback is a no-op. Without a proper TraceCallback (provided by the logger system), this "Trace" level messages shouldn't be outputted.
|
||||
func defaultTraceCallback(skip int, start bool, pid IDType, description string, parentPID IDType, typ string) {
|
||||
}
|
||||
|
||||
// TraceLogDisable disables (or revert the disabling) the trace log for the process lifecycle.
|
||||
// eg: the logger system shouldn't print the trace log for themselves, that's cycle dependency (Logger -> ProcessManager -> TraceCallback -> Logger ...)
|
||||
// Theoretically, such trace log should only be enabled when the logger system is ready with a proper level, so the default TraceCallback is a no-op.
|
||||
func TraceLogDisable(v bool) {
|
||||
if v {
|
||||
traceDisabled.Add(1)
|
||||
} else {
|
||||
log.Printf("end process %s: %s", pid, description)
|
||||
traceDisabled.Add(-1)
|
||||
}
|
||||
}
|
||||
|
||||
func Trace(start bool, pid IDType, description string, parentPID IDType, typ string) {
|
||||
if traceDisabled.Load() != 0 {
|
||||
// the traceDisabled counter is mainly for recursive calls, so no concurrency problem.
|
||||
// because the counter can't be 0 since the caller function hasn't returned (decreased the counter) yet.
|
||||
return
|
||||
}
|
||||
TraceCallback(1, start, pid, description, parentPID, typ)
|
||||
}
|
||||
|
||||
// Manager manages all processes and counts PIDs.
|
||||
type Manager struct {
|
||||
mutex sync.Mutex
|
||||
|
||||
@@ -5,16 +5,21 @@ package queue
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
|
||||
"code.gitea.io/gitea/modules/nosql"
|
||||
"code.gitea.io/gitea/modules/queue/lqinternal"
|
||||
|
||||
"gitea.com/lunny/levelqueue"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
type baseLevelQueue struct {
|
||||
internal *levelqueue.Queue
|
||||
conn string
|
||||
cfg *BaseConfig
|
||||
internal atomic.Pointer[levelqueue.Queue]
|
||||
|
||||
conn string
|
||||
cfg *BaseConfig
|
||||
db *leveldb.DB
|
||||
}
|
||||
|
||||
var _ baseQueue = (*baseLevelQueue)(nil)
|
||||
@@ -31,21 +36,23 @@ func newBaseLevelQueueSimple(cfg *BaseConfig) (baseQueue, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := &baseLevelQueue{conn: conn, cfg: cfg}
|
||||
q.internal, err = levelqueue.NewQueue(db, []byte(cfg.QueueFullName), false)
|
||||
q := &baseLevelQueue{conn: conn, cfg: cfg, db: db}
|
||||
lq, err := levelqueue.NewQueue(db, []byte(cfg.QueueFullName), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q.internal.Store(lq)
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (q *baseLevelQueue) PushItem(ctx context.Context, data []byte) error {
|
||||
return baseLevelQueueCommon(q.cfg, q.internal, nil).PushItem(ctx, data)
|
||||
c := baseLevelQueueCommon(q.cfg, nil, func() baseLevelQueuePushPoper { return q.internal.Load() })
|
||||
return c.PushItem(ctx, data)
|
||||
}
|
||||
|
||||
func (q *baseLevelQueue) PopItem(ctx context.Context) ([]byte, error) {
|
||||
return baseLevelQueueCommon(q.cfg, q.internal, nil).PopItem(ctx)
|
||||
c := baseLevelQueueCommon(q.cfg, nil, func() baseLevelQueuePushPoper { return q.internal.Load() })
|
||||
return c.PopItem(ctx)
|
||||
}
|
||||
|
||||
func (q *baseLevelQueue) HasItem(ctx context.Context, data []byte) (bool, error) {
|
||||
@@ -53,20 +60,24 @@ func (q *baseLevelQueue) HasItem(ctx context.Context, data []byte) (bool, error)
|
||||
}
|
||||
|
||||
func (q *baseLevelQueue) Len(ctx context.Context) (int, error) {
|
||||
return int(q.internal.Len()), nil
|
||||
return int(q.internal.Load().Len()), nil
|
||||
}
|
||||
|
||||
func (q *baseLevelQueue) Close() error {
|
||||
err := q.internal.Close()
|
||||
err := q.internal.Load().Close()
|
||||
_ = nosql.GetManager().CloseLevelDB(q.conn)
|
||||
q.db = nil // the db is not managed by us, it's managed by the nosql manager
|
||||
return err
|
||||
}
|
||||
|
||||
func (q *baseLevelQueue) RemoveAll(ctx context.Context) error {
|
||||
for q.internal.Len() > 0 {
|
||||
if _, err := q.internal.LPop(); err != nil {
|
||||
return err
|
||||
}
|
||||
lqinternal.RemoveLevelQueueKeys(q.db, []byte(q.cfg.QueueFullName))
|
||||
lq, err := levelqueue.NewQueue(q.db, []byte(q.cfg.QueueFullName), false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
old := q.internal.Load()
|
||||
q.internal.Store(lq)
|
||||
_ = old.Close() // Not ideal for concurrency. Luckily, the levelqueue only sets its db=nil because it doesn't manage the db, so far so good
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
// baseLevelQueuePushPoper is the common interface for levelqueue.Queue and levelqueue.UniqueQueue
|
||||
type baseLevelQueuePushPoper interface {
|
||||
RPush(data []byte) error
|
||||
LPop() ([]byte, error)
|
||||
@@ -24,9 +25,9 @@ type baseLevelQueuePushPoper interface {
|
||||
}
|
||||
|
||||
type baseLevelQueueCommonImpl struct {
|
||||
length int
|
||||
internal baseLevelQueuePushPoper
|
||||
mu *sync.Mutex
|
||||
length int
|
||||
internalFunc func() baseLevelQueuePushPoper
|
||||
mu *sync.Mutex
|
||||
}
|
||||
|
||||
func (q *baseLevelQueueCommonImpl) PushItem(ctx context.Context, data []byte) error {
|
||||
@@ -36,11 +37,11 @@ func (q *baseLevelQueueCommonImpl) PushItem(ctx context.Context, data []byte) er
|
||||
defer q.mu.Unlock()
|
||||
}
|
||||
|
||||
cnt := int(q.internal.Len())
|
||||
cnt := int(q.internalFunc().Len())
|
||||
if cnt >= q.length {
|
||||
return true, nil
|
||||
}
|
||||
retry, err = false, q.internal.RPush(data)
|
||||
retry, err = false, q.internalFunc().RPush(data)
|
||||
if err == levelqueue.ErrAlreadyInQueue {
|
||||
err = ErrAlreadyInQueue
|
||||
}
|
||||
@@ -55,7 +56,7 @@ func (q *baseLevelQueueCommonImpl) PopItem(ctx context.Context) ([]byte, error)
|
||||
defer q.mu.Unlock()
|
||||
}
|
||||
|
||||
data, err = q.internal.LPop()
|
||||
data, err = q.internalFunc().LPop()
|
||||
if err == levelqueue.ErrNotFound {
|
||||
return true, nil, nil
|
||||
}
|
||||
@@ -66,8 +67,8 @@ func (q *baseLevelQueueCommonImpl) PopItem(ctx context.Context) ([]byte, error)
|
||||
})
|
||||
}
|
||||
|
||||
func baseLevelQueueCommon(cfg *BaseConfig, internal baseLevelQueuePushPoper, mu *sync.Mutex) *baseLevelQueueCommonImpl {
|
||||
return &baseLevelQueueCommonImpl{length: cfg.Length, internal: internal}
|
||||
func baseLevelQueueCommon(cfg *BaseConfig, mu *sync.Mutex, internalFunc func() baseLevelQueuePushPoper) *baseLevelQueueCommonImpl {
|
||||
return &baseLevelQueueCommonImpl{length: cfg.Length, mu: mu, internalFunc: internalFunc}
|
||||
}
|
||||
|
||||
func prepareLevelDB(cfg *BaseConfig) (conn string, db *leveldb.DB, err error) {
|
||||
|
||||
@@ -6,9 +6,12 @@ package queue
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/queue/lqinternal"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"gitea.com/lunny/levelqueue"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
func TestBaseLevelDB(t *testing.T) {
|
||||
@@ -21,3 +24,55 @@ func TestBaseLevelDB(t *testing.T) {
|
||||
testQueueBasic(t, newBaseLevelQueueSimple, toBaseConfig("baseLevelQueue", setting.QueueSettings{Datadir: t.TempDir() + "/queue-test", Length: 10}), false)
|
||||
testQueueBasic(t, newBaseLevelQueueUnique, toBaseConfig("baseLevelQueueUnique", setting.QueueSettings{ConnStr: "leveldb://" + t.TempDir() + "/queue-test", Length: 10}), true)
|
||||
}
|
||||
|
||||
func TestCorruptedLevelQueue(t *testing.T) {
|
||||
// sometimes the levelqueue could be in a corrupted state, this test is to make sure it can recover from it
|
||||
dbDir := t.TempDir() + "/levelqueue-test"
|
||||
db, err := leveldb.OpenFile(dbDir, nil)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
assert.NoError(t, db.Put([]byte("other-key"), []byte("other-value"), nil))
|
||||
|
||||
nameQueuePrefix := []byte("queue_name")
|
||||
nameSetPrefix := []byte("set_name")
|
||||
lq, err := levelqueue.NewUniqueQueue(db, nameQueuePrefix, nameSetPrefix, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, lq.RPush([]byte("item-1")))
|
||||
|
||||
itemKey := lqinternal.QueueItemKeyBytes(nameQueuePrefix, 1)
|
||||
itemValue, err := db.Get(itemKey, nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("item-1"), itemValue)
|
||||
|
||||
// there should be 5 keys in db: queue low, queue high, 1 queue item, 1 set item, and "other-key"
|
||||
keys := lqinternal.ListLevelQueueKeys(db)
|
||||
assert.Len(t, keys, 5)
|
||||
|
||||
// delete the queue item key, to corrupt the queue
|
||||
assert.NoError(t, db.Delete(itemKey, nil))
|
||||
// now the queue is corrupted, it never works again
|
||||
_, err = lq.LPop()
|
||||
assert.ErrorIs(t, err, levelqueue.ErrNotFound)
|
||||
assert.NoError(t, lq.Close())
|
||||
|
||||
// remove all the queue related keys to reset the queue
|
||||
lqinternal.RemoveLevelQueueKeys(db, nameQueuePrefix)
|
||||
lqinternal.RemoveLevelQueueKeys(db, nameSetPrefix)
|
||||
// now there should be only 1 key in db: "other-key"
|
||||
keys = lqinternal.ListLevelQueueKeys(db)
|
||||
assert.Len(t, keys, 1)
|
||||
assert.Equal(t, []byte("other-key"), keys[0])
|
||||
|
||||
// re-create a queue from db
|
||||
lq, err = levelqueue.NewUniqueQueue(db, nameQueuePrefix, nameSetPrefix, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NoError(t, lq.RPush([]byte("item-new-1")))
|
||||
// now the queue works again
|
||||
itemValue, err = lq.LPop()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("item-new-1"), itemValue)
|
||||
assert.NoError(t, lq.Close())
|
||||
}
|
||||
|
||||
@@ -6,18 +6,21 @@ package queue
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"unsafe"
|
||||
"sync/atomic"
|
||||
|
||||
"code.gitea.io/gitea/modules/nosql"
|
||||
"code.gitea.io/gitea/modules/queue/lqinternal"
|
||||
|
||||
"gitea.com/lunny/levelqueue"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
type baseLevelQueueUnique struct {
|
||||
internal *levelqueue.UniqueQueue
|
||||
conn string
|
||||
cfg *BaseConfig
|
||||
internal atomic.Pointer[levelqueue.UniqueQueue]
|
||||
|
||||
conn string
|
||||
cfg *BaseConfig
|
||||
db *leveldb.DB
|
||||
|
||||
mu sync.Mutex // the levelqueue.UniqueQueue is not thread-safe, there is no mutex protecting the underlying queue&set together
|
||||
}
|
||||
@@ -29,39 +32,42 @@ func newBaseLevelQueueUnique(cfg *BaseConfig) (baseQueue, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
q := &baseLevelQueueUnique{conn: conn, cfg: cfg}
|
||||
q.internal, err = levelqueue.NewUniqueQueue(db, []byte(cfg.QueueFullName), []byte(cfg.SetFullName), false)
|
||||
q := &baseLevelQueueUnique{conn: conn, cfg: cfg, db: db}
|
||||
lq, err := levelqueue.NewUniqueQueue(db, []byte(cfg.QueueFullName), []byte(cfg.SetFullName), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q.internal.Store(lq)
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (q *baseLevelQueueUnique) PushItem(ctx context.Context, data []byte) error {
|
||||
return baseLevelQueueCommon(q.cfg, q.internal, &q.mu).PushItem(ctx, data)
|
||||
c := baseLevelQueueCommon(q.cfg, &q.mu, func() baseLevelQueuePushPoper { return q.internal.Load() })
|
||||
return c.PushItem(ctx, data)
|
||||
}
|
||||
|
||||
func (q *baseLevelQueueUnique) PopItem(ctx context.Context) ([]byte, error) {
|
||||
return baseLevelQueueCommon(q.cfg, q.internal, &q.mu).PopItem(ctx)
|
||||
c := baseLevelQueueCommon(q.cfg, &q.mu, func() baseLevelQueuePushPoper { return q.internal.Load() })
|
||||
return c.PopItem(ctx)
|
||||
}
|
||||
|
||||
func (q *baseLevelQueueUnique) HasItem(ctx context.Context, data []byte) (bool, error) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
return q.internal.Has(data)
|
||||
return q.internal.Load().Has(data)
|
||||
}
|
||||
|
||||
func (q *baseLevelQueueUnique) Len(ctx context.Context) (int, error) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
return int(q.internal.Len()), nil
|
||||
return int(q.internal.Load().Len()), nil
|
||||
}
|
||||
|
||||
func (q *baseLevelQueueUnique) Close() error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
err := q.internal.Close()
|
||||
err := q.internal.Load().Close()
|
||||
q.db = nil // the db is not managed by us, it's managed by the nosql manager
|
||||
_ = nosql.GetManager().CloseLevelDB(q.conn)
|
||||
return err
|
||||
}
|
||||
@@ -69,28 +75,14 @@ func (q *baseLevelQueueUnique) Close() error {
|
||||
func (q *baseLevelQueueUnique) RemoveAll(ctx context.Context) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
type levelUniqueQueue struct {
|
||||
q *levelqueue.Queue
|
||||
set *levelqueue.Set
|
||||
db *leveldb.DB
|
||||
}
|
||||
lq := (*levelUniqueQueue)(unsafe.Pointer(q.internal))
|
||||
|
||||
for lq.q.Len() > 0 {
|
||||
if _, err := lq.q.LPop(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// the "set" must be cleared after the "list" because there is no transaction.
|
||||
// it's better to have duplicate items than losing items.
|
||||
members, err := lq.set.Members()
|
||||
lqinternal.RemoveLevelQueueKeys(q.db, []byte(q.cfg.QueueFullName))
|
||||
lqinternal.RemoveLevelQueueKeys(q.db, []byte(q.cfg.SetFullName))
|
||||
lq, err := levelqueue.NewUniqueQueue(q.db, []byte(q.cfg.QueueFullName), []byte(q.cfg.SetFullName), false)
|
||||
if err != nil {
|
||||
return err // seriously corrupted
|
||||
}
|
||||
for _, v := range members {
|
||||
_, _ = lq.set.Remove(v)
|
||||
return err
|
||||
}
|
||||
old := q.internal.Load()
|
||||
q.internal.Store(lq)
|
||||
_ = old.Close() // Not ideal for concurrency. Luckily, the levelqueue only sets its db=nil because it doesn't manage the db, so far so good
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package lqinternal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
)
|
||||
|
||||
func QueueItemIDBytes(id int64) []byte {
|
||||
buf := make([]byte, 8)
|
||||
binary.PutVarint(buf, id)
|
||||
return buf
|
||||
}
|
||||
|
||||
func QueueItemKeyBytes(prefix []byte, id int64) []byte {
|
||||
key := make([]byte, len(prefix), len(prefix)+1+8)
|
||||
copy(key, prefix)
|
||||
key = append(key, '-')
|
||||
return append(key, QueueItemIDBytes(id)...)
|
||||
}
|
||||
|
||||
func RemoveLevelQueueKeys(db *leveldb.DB, namePrefix []byte) {
|
||||
keyPrefix := make([]byte, len(namePrefix)+1)
|
||||
copy(keyPrefix, namePrefix)
|
||||
keyPrefix[len(namePrefix)] = '-'
|
||||
|
||||
it := db.NewIterator(nil, &opt.ReadOptions{Strict: opt.NoStrict})
|
||||
defer it.Release()
|
||||
for it.Next() {
|
||||
if bytes.HasPrefix(it.Key(), keyPrefix) {
|
||||
_ = db.Delete(it.Key(), nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ListLevelQueueKeys(db *leveldb.DB) (res [][]byte) {
|
||||
it := db.NewIterator(nil, &opt.ReadOptions{Strict: opt.NoStrict})
|
||||
defer it.Release()
|
||||
for it.Next() {
|
||||
res = append(res, it.Key())
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -399,6 +399,10 @@ func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibili
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = repo_model.ClearRepoStars(ctx, repo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Create/Remove git-daemon-export-ok for git-daemon...
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -26,8 +25,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -240,14 +237,14 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
|
||||
|
||||
// cleanUpMigrateGitConfig removes mirror info which prevents "push --all".
|
||||
// This also removes possible user credentials.
|
||||
func cleanUpMigrateGitConfig(configPath string) error {
|
||||
cfg, err := ini.Load(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open config file: %w", err)
|
||||
}
|
||||
cfg.DeleteSection("remote \"origin\"")
|
||||
if err = cfg.SaveToIndent(configPath, "\t"); err != nil {
|
||||
return fmt.Errorf("save config file: %w", err)
|
||||
func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error {
|
||||
cmd := git.NewCommand(ctx, "remote", "rm", "origin")
|
||||
// if the origin does not exist
|
||||
_, stderr, err := cmd.RunStdString(&git.RunOpts{
|
||||
Dir: repoPath,
|
||||
})
|
||||
if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -270,7 +267,7 @@ func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo
|
||||
}
|
||||
|
||||
if repo.HasWiki() {
|
||||
if err := cleanUpMigrateGitConfig(path.Join(repo.WikiPath(), "config")); err != nil {
|
||||
if err := cleanUpMigrateGitConfig(ctx, repo.WikiPath()); err != nil {
|
||||
return repo, fmt.Errorf("cleanUpMigrateGitConfig (wiki): %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
const escapeRegexpString = "_0[xX](([0-9a-fA-F][0-9a-fA-F])+)_"
|
||||
@@ -89,7 +87,7 @@ func decodeEnvironmentKey(prefixGitea, suffixFile, envKey string) (ok bool, sect
|
||||
return ok, section, key, useFileValue
|
||||
}
|
||||
|
||||
func EnvironmentToConfig(cfg *ini.File, prefixGitea, suffixFile string, envs []string) (changed bool) {
|
||||
func EnvironmentToConfig(cfg ConfigProvider, prefixGitea, suffixFile string, envs []string) (changed bool) {
|
||||
for _, kv := range envs {
|
||||
idx := strings.IndexByte(kv, '=')
|
||||
if idx < 0 {
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
func TestDecodeEnvSectionKey(t *testing.T) {
|
||||
@@ -71,15 +70,15 @@ func TestDecodeEnvironmentKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEnvironmentToConfig(t *testing.T) {
|
||||
cfg := ini.Empty()
|
||||
cfg, _ := NewConfigProviderFromData("")
|
||||
|
||||
changed := EnvironmentToConfig(cfg, "GITEA__", "__FILE", nil)
|
||||
assert.False(t, changed)
|
||||
|
||||
cfg, err := ini.Load([]byte(`
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[sec]
|
||||
key = old
|
||||
`))
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
|
||||
changed = EnvironmentToConfig(cfg, "GITEA__", "__FILE", []string{"GITEA__sec__key=new"})
|
||||
|
||||
@@ -8,36 +8,71 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
ini "gopkg.in/ini.v1"
|
||||
"gopkg.in/ini.v1" //nolint:depguard
|
||||
)
|
||||
|
||||
type ConfigKey interface {
|
||||
Name() string
|
||||
Value() string
|
||||
SetValue(v string)
|
||||
|
||||
In(defaultVal string, candidates []string) string
|
||||
String() string
|
||||
Strings(delim string) []string
|
||||
|
||||
MustString(defaultVal string) string
|
||||
MustBool(defaultVal ...bool) bool
|
||||
MustInt(defaultVal ...int) int
|
||||
MustInt64(defaultVal ...int64) int64
|
||||
MustDuration(defaultVal ...time.Duration) time.Duration
|
||||
}
|
||||
|
||||
type ConfigSection interface {
|
||||
Name() string
|
||||
MapTo(interface{}) error
|
||||
MapTo(any) error
|
||||
HasKey(key string) bool
|
||||
NewKey(name, value string) (*ini.Key, error)
|
||||
Key(key string) *ini.Key
|
||||
Keys() []*ini.Key
|
||||
ChildSections() []*ini.Section
|
||||
NewKey(name, value string) (ConfigKey, error)
|
||||
Key(key string) ConfigKey
|
||||
Keys() []ConfigKey
|
||||
ChildSections() []ConfigSection
|
||||
}
|
||||
|
||||
// ConfigProvider represents a config provider
|
||||
type ConfigProvider interface {
|
||||
Section(section string) ConfigSection
|
||||
Sections() []ConfigSection
|
||||
NewSection(name string) (ConfigSection, error)
|
||||
GetSection(name string) (ConfigSection, error)
|
||||
Save() error
|
||||
SaveTo(filename string) error
|
||||
}
|
||||
|
||||
type iniConfigProvider struct {
|
||||
opts *Options
|
||||
ini *ini.File
|
||||
newFile bool // whether the file has not existed previously
|
||||
}
|
||||
|
||||
type iniConfigSection struct {
|
||||
sec *ini.Section
|
||||
}
|
||||
|
||||
var (
|
||||
_ ConfigProvider = (*iniConfigProvider)(nil)
|
||||
_ ConfigSection = (*iniConfigSection)(nil)
|
||||
_ ConfigKey = (*ini.Key)(nil)
|
||||
)
|
||||
|
||||
// ConfigSectionKey only searches the keys in the given section, but it is O(n).
|
||||
// ini package has a special behavior: with "[sec] a=1" and an empty "[sec.sub]",
|
||||
// then in "[sec.sub]", Key()/HasKey() can always see "a=1" because it always tries parent sections.
|
||||
// It returns nil if the key doesn't exist.
|
||||
func ConfigSectionKey(sec ConfigSection, key string) *ini.Key {
|
||||
func ConfigSectionKey(sec ConfigSection, key string) ConfigKey {
|
||||
if sec == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -64,7 +99,7 @@ func ConfigSectionKeyString(sec ConfigSection, key string, def ...string) string
|
||||
// and the returned key is safe to be used with "MustXxx", it doesn't change the parent's values.
|
||||
// Otherwise, ini.Section.Key().MustXxx would pollute the parent section's keys.
|
||||
// It never returns nil.
|
||||
func ConfigInheritedKey(sec ConfigSection, key string) *ini.Key {
|
||||
func ConfigInheritedKey(sec ConfigSection, key string) ConfigKey {
|
||||
k := sec.Key(key)
|
||||
if k != nil && k.String() != "" {
|
||||
newKey, _ := sec.NewKey(k.Name(), k.String())
|
||||
@@ -85,41 +120,64 @@ func ConfigInheritedKeyString(sec ConfigSection, key string, def ...string) stri
|
||||
return ""
|
||||
}
|
||||
|
||||
type iniFileConfigProvider struct {
|
||||
opts *Options
|
||||
*ini.File
|
||||
newFile bool // whether the file has not existed previously
|
||||
func (s *iniConfigSection) Name() string {
|
||||
return s.sec.Name()
|
||||
}
|
||||
|
||||
// NewConfigProviderFromData this function is only for testing
|
||||
func (s *iniConfigSection) MapTo(v any) error {
|
||||
return s.sec.MapTo(v)
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) HasKey(key string) bool {
|
||||
return s.sec.HasKey(key)
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) NewKey(name, value string) (ConfigKey, error) {
|
||||
return s.sec.NewKey(name, value)
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) Key(key string) ConfigKey {
|
||||
return s.sec.Key(key)
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) Keys() (keys []ConfigKey) {
|
||||
for _, k := range s.sec.Keys() {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (s *iniConfigSection) ChildSections() (sections []ConfigSection) {
|
||||
for _, s := range s.sec.ChildSections() {
|
||||
sections = append(sections, &iniConfigSection{s})
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
// NewConfigProviderFromData this function is mainly for testing purpose
|
||||
func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
|
||||
var cfg *ini.File
|
||||
var err error
|
||||
if configContent == "" {
|
||||
cfg = ini.Empty()
|
||||
} else {
|
||||
cfg, err = ini.Load(strings.NewReader(configContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := ini.Load(strings.NewReader(configContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg.NameMapper = ini.SnackCase
|
||||
return &iniFileConfigProvider{
|
||||
File: cfg,
|
||||
return &iniConfigProvider{
|
||||
ini: cfg,
|
||||
newFile: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
CustomConf string // the ini file path
|
||||
AllowEmpty bool // whether not finding configuration files is allowed (only true for the tests)
|
||||
ExtraConfig string
|
||||
DisableLoadCommonSettings bool
|
||||
CustomConf string // the ini file path
|
||||
AllowEmpty bool // whether not finding configuration files is allowed
|
||||
ExtraConfig string
|
||||
|
||||
DisableLoadCommonSettings bool // only used by "Init()", not used by "NewConfigProvider()"
|
||||
}
|
||||
|
||||
// newConfigProviderFromFile load configuration from file.
|
||||
// NewConfigProviderFromFile load configuration from file.
|
||||
// NOTE: do not print any log except error.
|
||||
func newConfigProviderFromFile(opts *Options) (*iniFileConfigProvider, error) {
|
||||
func NewConfigProviderFromFile(opts *Options) (ConfigProvider, error) {
|
||||
cfg := ini.Empty()
|
||||
newFile := true
|
||||
|
||||
@@ -147,61 +205,77 @@ func newConfigProviderFromFile(opts *Options) (*iniFileConfigProvider, error) {
|
||||
}
|
||||
|
||||
cfg.NameMapper = ini.SnackCase
|
||||
return &iniFileConfigProvider{
|
||||
return &iniConfigProvider{
|
||||
opts: opts,
|
||||
File: cfg,
|
||||
ini: cfg,
|
||||
newFile: newFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *iniFileConfigProvider) Section(section string) ConfigSection {
|
||||
return p.File.Section(section)
|
||||
func (p *iniConfigProvider) Section(section string) ConfigSection {
|
||||
return &iniConfigSection{sec: p.ini.Section(section)}
|
||||
}
|
||||
|
||||
func (p *iniFileConfigProvider) NewSection(name string) (ConfigSection, error) {
|
||||
return p.File.NewSection(name)
|
||||
func (p *iniConfigProvider) Sections() (sections []ConfigSection) {
|
||||
for _, s := range p.ini.Sections() {
|
||||
sections = append(sections, &iniConfigSection{s})
|
||||
}
|
||||
return sections
|
||||
}
|
||||
|
||||
func (p *iniFileConfigProvider) GetSection(name string) (ConfigSection, error) {
|
||||
return p.File.GetSection(name)
|
||||
func (p *iniConfigProvider) NewSection(name string) (ConfigSection, error) {
|
||||
sec, err := p.ini.NewSection(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iniConfigSection{sec: sec}, nil
|
||||
}
|
||||
|
||||
// Save save the content into file
|
||||
func (p *iniFileConfigProvider) Save() error {
|
||||
if p.opts.CustomConf == "" {
|
||||
func (p *iniConfigProvider) GetSection(name string) (ConfigSection, error) {
|
||||
sec, err := p.ini.GetSection(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iniConfigSection{sec: sec}, nil
|
||||
}
|
||||
|
||||
// Save saves the content into file
|
||||
func (p *iniConfigProvider) Save() error {
|
||||
filename := p.opts.CustomConf
|
||||
if filename == "" {
|
||||
if !p.opts.AllowEmpty {
|
||||
return fmt.Errorf("custom config path must not be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.newFile {
|
||||
if err := os.MkdirAll(filepath.Dir(CustomConf), os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to create '%s': %v", CustomConf, err)
|
||||
if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to create '%s': %v", filename, err)
|
||||
}
|
||||
}
|
||||
if err := p.SaveTo(p.opts.CustomConf); err != nil {
|
||||
return fmt.Errorf("failed to save '%s': %v", p.opts.CustomConf, err)
|
||||
if err := p.ini.SaveTo(filename); err != nil {
|
||||
return fmt.Errorf("failed to save '%s': %v", filename, err)
|
||||
}
|
||||
|
||||
// Change permissions to be more restrictive
|
||||
fi, err := os.Stat(CustomConf)
|
||||
fi, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to determine current conf file permissions: %v", err)
|
||||
}
|
||||
|
||||
if fi.Mode().Perm() > 0o600 {
|
||||
if err = os.Chmod(CustomConf, 0o600); err != nil {
|
||||
if err = os.Chmod(filename, 0o600); err != nil {
|
||||
log.Warn("Failed changing conf file permissions to -rw-------. Consider changing them manually.")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// a file is an implementation ConfigProvider and other implementations are possible, i.e. from docker, k8s, …
|
||||
var _ ConfigProvider = &iniFileConfigProvider{}
|
||||
func (p *iniConfigProvider) SaveTo(filename string) error {
|
||||
return p.ini.SaveTo(filename)
|
||||
}
|
||||
|
||||
func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting interface{}) {
|
||||
func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) {
|
||||
if err := rootCfg.Section(sectionName).MapTo(setting); err != nil {
|
||||
log.Fatal("Failed to map %s settings: %v", sectionName, err)
|
||||
}
|
||||
@@ -219,3 +293,23 @@ func deprecatedSettingDB(rootCfg ConfigProvider, oldSection, oldKey string) {
|
||||
log.Error("Deprecated `[%s]` `%s` present which has been copied to database table sys_setting", oldSection, oldKey)
|
||||
}
|
||||
}
|
||||
|
||||
// NewConfigProviderForLocale loads locale configuration from source and others. "string" if for a local file path, "[]byte" is for INI content
|
||||
func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, error) {
|
||||
iniFile, err := ini.LoadSources(ini.LoadOptions{
|
||||
IgnoreInlineComment: true,
|
||||
UnescapeValueCommentSymbols: true,
|
||||
}, source, others...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load locale ini: %w", err)
|
||||
}
|
||||
iniFile.BlockMode = false
|
||||
return &iniConfigProvider{
|
||||
ini: iniFile,
|
||||
newFile: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
ini.PrettyFormat = false
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package setting
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -64,3 +65,57 @@ key = 123
|
||||
assert.Equal(t, "", ConfigSectionKeyString(sec, "empty"))
|
||||
assert.Equal(t, "def", ConfigSectionKeyString(secSub, "empty"))
|
||||
}
|
||||
|
||||
func TestNewConfigProviderFromFile(t *testing.T) {
|
||||
_, err := NewConfigProviderFromFile(&Options{CustomConf: "no-such.ini", AllowEmpty: false})
|
||||
assert.ErrorContains(t, err, "unable to find configuration file")
|
||||
|
||||
// load non-existing file and save
|
||||
testFile := t.TempDir() + "/test.ini"
|
||||
testFile1 := t.TempDir() + "/test1.ini"
|
||||
cfg, err := NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true})
|
||||
assert.NoError(t, err)
|
||||
|
||||
sec, _ := cfg.NewSection("foo")
|
||||
_, _ = sec.NewKey("k1", "a")
|
||||
assert.NoError(t, cfg.Save())
|
||||
_, _ = sec.NewKey("k2", "b")
|
||||
assert.NoError(t, cfg.SaveTo(testFile1))
|
||||
|
||||
bs, err := os.ReadFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[foo]\nk1=a\n", string(bs))
|
||||
|
||||
bs, err = os.ReadFile(testFile1)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[foo]\nk1=a\nk2=b\n", string(bs))
|
||||
|
||||
// load existing file and save
|
||||
cfg, err = NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "a", cfg.Section("foo").Key("k1").String())
|
||||
sec, _ = cfg.NewSection("bar")
|
||||
_, _ = sec.NewKey("k1", "b")
|
||||
assert.NoError(t, cfg.Save())
|
||||
bs, err = os.ReadFile(testFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "[foo]\nk1=a\n\n[bar]\nk1=b\n", string(bs))
|
||||
}
|
||||
|
||||
func TestNewConfigProviderForLocale(t *testing.T) {
|
||||
// load locale from file
|
||||
localeFile := t.TempDir() + "/locale.ini"
|
||||
_ = os.WriteFile(localeFile, []byte(`k1=a`), 0o644)
|
||||
cfg, err := NewConfigProviderForLocale(localeFile)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "a", cfg.Section("").Key("k1").String())
|
||||
|
||||
// load locale from bytes
|
||||
cfg, err = NewConfigProviderForLocale([]byte("k1=foo\nk2=bar"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo", cfg.Section("").Key("k1").String())
|
||||
cfg, err = NewConfigProviderForLocale([]byte("k1=foo\nk2=bar"), []byte("k2=xxx"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "foo", cfg.Section("").Key("k1").String())
|
||||
assert.Equal(t, "xxx", cfg.Section("").Key("k2").String())
|
||||
}
|
||||
|
||||
+29
-19
@@ -16,10 +16,7 @@ var Git = struct {
|
||||
Path string
|
||||
HomePath string
|
||||
DisableDiffHighlight bool
|
||||
Reflog struct {
|
||||
Enabled bool
|
||||
Expiration int
|
||||
} `ini:"git.reflog"`
|
||||
|
||||
MaxGitDiffLines int
|
||||
MaxGitDiffLineCharacters int
|
||||
MaxGitDiffFiles int
|
||||
@@ -42,13 +39,6 @@ var Git = struct {
|
||||
GC int `ini:"GC"`
|
||||
} `ini:"git.timeout"`
|
||||
}{
|
||||
Reflog: struct {
|
||||
Enabled bool
|
||||
Expiration int
|
||||
}{
|
||||
Enabled: true,
|
||||
Expiration: 90,
|
||||
},
|
||||
DisableDiffHighlight: false,
|
||||
MaxGitDiffLines: 1000,
|
||||
MaxGitDiffLineCharacters: 5000,
|
||||
@@ -79,9 +69,19 @@ var Git = struct {
|
||||
},
|
||||
}
|
||||
|
||||
var GitConfig = struct {
|
||||
Options map[string]string
|
||||
}{
|
||||
type GitConfigType struct {
|
||||
Options map[string]string // git config key is case-insensitive, always use lower-case
|
||||
}
|
||||
|
||||
func (c *GitConfigType) SetOption(key, val string) {
|
||||
c.Options[strings.ToLower(key)] = val
|
||||
}
|
||||
|
||||
func (c *GitConfigType) GetOption(key string) string {
|
||||
return c.Options[strings.ToLower(key)]
|
||||
}
|
||||
|
||||
var GitConfig = GitConfigType{
|
||||
Options: make(map[string]string),
|
||||
}
|
||||
|
||||
@@ -93,12 +93,22 @@ func loadGitFrom(rootCfg ConfigProvider) {
|
||||
|
||||
secGitConfig := rootCfg.Section("git.config")
|
||||
GitConfig.Options = make(map[string]string)
|
||||
for _, key := range secGitConfig.Keys() {
|
||||
// git config key is case-insensitive, so always use lower-case
|
||||
GitConfig.Options[strings.ToLower(key.Name())] = key.String()
|
||||
GitConfig.SetOption("diff.algorithm", "histogram")
|
||||
GitConfig.SetOption("core.logAllRefUpdates", "true")
|
||||
GitConfig.SetOption("gc.reflogExpire", "90")
|
||||
|
||||
secGitReflog := rootCfg.Section("git.reflog")
|
||||
if secGitReflog.HasKey("ENABLED") {
|
||||
deprecatedSetting(rootCfg, "git.reflog", "ENABLED", "git.config", "core.logAllRefUpdates", "1.21")
|
||||
GitConfig.SetOption("core.logAllRefUpdates", secGitReflog.Key("ENABLED").In("true", []string{"true", "false"}))
|
||||
}
|
||||
if _, ok := GitConfig.Options["diff.algorithm"]; !ok {
|
||||
GitConfig.Options["diff.algorithm"] = "histogram"
|
||||
if secGitReflog.HasKey("EXPIRATION") {
|
||||
deprecatedSetting(rootCfg, "git.reflog", "EXPIRATION", "git.config", "core.reflogExpire", "1.21")
|
||||
GitConfig.SetOption("gc.reflogExpire", secGitReflog.Key("EXPIRATION").String())
|
||||
}
|
||||
|
||||
for _, key := range secGitConfig.Keys() {
|
||||
GitConfig.SetOption(key.Name(), key.String())
|
||||
}
|
||||
|
||||
Git.HomePath = sec.Key("HOME_PATH").MustString("home")
|
||||
|
||||
@@ -23,8 +23,6 @@ a.b = 1
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadGitFrom(cfg)
|
||||
|
||||
assert.Len(t, GitConfig.Options, 2)
|
||||
assert.EqualValues(t, "1", GitConfig.Options["a.b"])
|
||||
assert.EqualValues(t, "histogram", GitConfig.Options["diff.algorithm"])
|
||||
|
||||
@@ -34,7 +32,34 @@ diff.algorithm = other
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadGitFrom(cfg)
|
||||
|
||||
assert.Len(t, GitConfig.Options, 1)
|
||||
assert.EqualValues(t, "other", GitConfig.Options["diff.algorithm"])
|
||||
}
|
||||
|
||||
func TestGitReflog(t *testing.T) {
|
||||
oldGit := Git
|
||||
oldGitConfig := GitConfig
|
||||
defer func() {
|
||||
Git = oldGit
|
||||
GitConfig = oldGitConfig
|
||||
}()
|
||||
|
||||
// default reflog config without legacy options
|
||||
cfg, err := NewConfigProviderFromData(``)
|
||||
assert.NoError(t, err)
|
||||
loadGitFrom(cfg)
|
||||
|
||||
assert.EqualValues(t, "true", GitConfig.GetOption("core.logAllRefUpdates"))
|
||||
assert.EqualValues(t, "90", GitConfig.GetOption("gc.reflogExpire"))
|
||||
|
||||
// custom reflog config by legacy options
|
||||
cfg, err = NewConfigProviderFromData(`
|
||||
[git.reflog]
|
||||
ENABLED = false
|
||||
EXPIRATION = 123
|
||||
`)
|
||||
assert.NoError(t, err)
|
||||
loadGitFrom(cfg)
|
||||
|
||||
assert.EqualValues(t, "false", GitConfig.GetOption("core.logAllRefUpdates"))
|
||||
assert.EqualValues(t, "123", GitConfig.GetOption("gc.reflogExpire"))
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ func Init(opts *Options) {
|
||||
opts.CustomConf = CustomConf
|
||||
}
|
||||
var err error
|
||||
CfgProvider, err = newConfigProviderFromFile(opts)
|
||||
CfgProvider, err = NewConfigProviderFromFile(opts)
|
||||
if err != nil {
|
||||
log.Fatal("Init[%v]: %v", opts, err)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ var UI = struct {
|
||||
GraphMaxCommitNum int
|
||||
CodeCommentLines int
|
||||
ReactionMaxUserNum int
|
||||
ThemeColorMetaTag string
|
||||
MaxDisplayFileSize int64
|
||||
ShowUserEmail bool
|
||||
DefaultShowFullName bool
|
||||
@@ -33,7 +32,6 @@ var UI = struct {
|
||||
CustomEmojis []string
|
||||
CustomEmojisMap map[string]string `ini:"-"`
|
||||
SearchRepoDescription bool
|
||||
UseServiceWorker bool
|
||||
OnlyShowRelevantRepos bool
|
||||
|
||||
Notification struct {
|
||||
@@ -77,7 +75,6 @@ var UI = struct {
|
||||
GraphMaxCommitNum: 100,
|
||||
CodeCommentLines: 4,
|
||||
ReactionMaxUserNum: 10,
|
||||
ThemeColorMetaTag: ``,
|
||||
MaxDisplayFileSize: 8388608,
|
||||
DefaultTheme: `auto`,
|
||||
Themes: []string{`auto`, `gitea`, `arc-green`},
|
||||
@@ -138,7 +135,6 @@ func loadUIFrom(rootCfg ConfigProvider) {
|
||||
UI.ShowUserEmail = sec.Key("SHOW_USER_EMAIL").MustBool(true)
|
||||
UI.DefaultShowFullName = sec.Key("DEFAULT_SHOW_FULL_NAME").MustBool(false)
|
||||
UI.SearchRepoDescription = sec.Key("SEARCH_REPO_DESCRIPTION").MustBool(true)
|
||||
UI.UseServiceWorker = sec.Key("USE_SERVICE_WORKER").MustBool(false)
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -64,6 +64,37 @@ func (o *UpdateFileOptions) Branch() string {
|
||||
return o.FileOptions.BranchName
|
||||
}
|
||||
|
||||
// ChangeFileOperation for creating, updating or deleting a file
|
||||
type ChangeFileOperation struct {
|
||||
// indicates what to do with the file
|
||||
// required: true
|
||||
// enum: create,update,delete
|
||||
Operation string `json:"operation" binding:"Required"`
|
||||
// path to the existing or new file
|
||||
// required: true
|
||||
Path string `json:"path" binding:"Required;MaxSize(500)"`
|
||||
// new or updated file content, must be base64 encoded
|
||||
Content string `json:"content"`
|
||||
// sha is the SHA for the file that already exists, required for update or delete
|
||||
SHA string `json:"sha"`
|
||||
// old path of the file to move
|
||||
FromPath string `json:"from_path"`
|
||||
}
|
||||
|
||||
// ChangeFilesOptions options for creating, updating or deleting multiple files
|
||||
// Note: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)
|
||||
type ChangeFilesOptions struct {
|
||||
FileOptions
|
||||
// list of file operations
|
||||
// required: true
|
||||
Files []*ChangeFileOperation `json:"files" binding:"Required"`
|
||||
}
|
||||
|
||||
// Branch returns branch name
|
||||
func (o *ChangeFilesOptions) Branch() string {
|
||||
return o.FileOptions.BranchName
|
||||
}
|
||||
|
||||
// FileOptionInterface provides a unified interface for the different file options
|
||||
type FileOptionInterface interface {
|
||||
Branch() string
|
||||
@@ -126,6 +157,13 @@ type FileResponse struct {
|
||||
Verification *PayloadCommitVerification `json:"verification"`
|
||||
}
|
||||
|
||||
// FilesResponse contains information about multiple files from a repo
|
||||
type FilesResponse struct {
|
||||
Files []*ContentsResponse `json:"files"`
|
||||
Commit *FileCommitResponse `json:"commit"`
|
||||
Verification *PayloadCommitVerification `json:"verification"`
|
||||
}
|
||||
|
||||
// FileDeleteResponse contains information about a repo's file that was deleted
|
||||
type FileDeleteResponse struct {
|
||||
Content interface{} `json:"content"` // to be set to nil
|
||||
|
||||
@@ -118,9 +118,6 @@ func NewFuncMap() template.FuncMap {
|
||||
"CustomEmojis": func() map[string]string {
|
||||
return setting.UI.CustomEmojisMap
|
||||
},
|
||||
"ThemeColorMetaTag": func() string {
|
||||
return setting.UI.ThemeColorMetaTag
|
||||
},
|
||||
"MetaAuthor": func() string {
|
||||
return setting.UI.Meta.Author
|
||||
},
|
||||
@@ -130,9 +127,6 @@ func NewFuncMap() template.FuncMap {
|
||||
"MetaKeywords": func() string {
|
||||
return setting.UI.Meta.Keywords
|
||||
},
|
||||
"UseServiceWorker": func() bool {
|
||||
return setting.UI.UseServiceWorker
|
||||
},
|
||||
"EnableTimetracking": func() bool {
|
||||
return setting.Service.EnableTimetracking
|
||||
},
|
||||
|
||||
@@ -5,9 +5,14 @@ package test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RedirectURL returns the redirect URL of a http response.
|
||||
func RedirectURL(resp http.ResponseWriter) string {
|
||||
return resp.Header().Get("Location")
|
||||
}
|
||||
|
||||
func IsNormalPageCompleted(s string) bool {
|
||||
return strings.Contains(s, `<footer class="page-footer"`) && strings.Contains(s, `</html>`)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
// This file implements the static LocaleStore that will not watch for changes
|
||||
@@ -47,14 +46,10 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more
|
||||
l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string)}
|
||||
store.localeMap[l.langName] = l
|
||||
|
||||
iniFile, err := ini.LoadSources(ini.LoadOptions{
|
||||
IgnoreInlineComment: true,
|
||||
UnescapeValueCommentSymbols: true,
|
||||
}, source, moreSource)
|
||||
iniFile, err := setting.NewConfigProviderForLocale(source, moreSource)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load ini: %w", err)
|
||||
}
|
||||
iniFile.BlockMode = false
|
||||
|
||||
for _, section := range iniFile.Sections() {
|
||||
for _, key := range section.Keys() {
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
package util
|
||||
|
||||
import "unicode/utf8"
|
||||
import (
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// in UTF8 "…" is 3 bytes so doesn't really gain us anything...
|
||||
const (
|
||||
@@ -35,3 +38,17 @@ func SplitStringAtByteN(input string, n int) (left, right string) {
|
||||
|
||||
return input[:end] + utf8Ellipsis, utf8Ellipsis + input[end:]
|
||||
}
|
||||
|
||||
// SplitTrimSpace splits the string at given separator and trims leading and trailing space
|
||||
func SplitTrimSpace(input, sep string) []string {
|
||||
// replace CRLF with LF
|
||||
input = strings.ReplaceAll(input, "\r\n", "\n")
|
||||
|
||||
var stringList []string
|
||||
for _, s := range strings.Split(input, sep) {
|
||||
// trim leading and trailing space
|
||||
stringList = append(stringList, strings.TrimSpace(s))
|
||||
}
|
||||
|
||||
return stringList
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user