diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f52da3fa5d..abb9bde232 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -403,7 +403,7 @@ module.exports = { 'github/a11y-svg-has-accessible-name': [0], 'github/array-foreach': [0], 'github/async-currenttarget': [2], - 'github/async-preventdefault': [2], + 'github/async-preventdefault': [0], // https://github.com/github/eslint-plugin-github/issues/599 'github/authenticity-token': [0], 'github/get-attribute': [0], 'github/js-class-name': [0], diff --git a/cmd/web.go b/cmd/web.go index f8217758e5..dc5c6de48a 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -18,10 +18,12 @@ import ( "code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/graceful" + "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers/install" @@ -218,6 +220,8 @@ func serveInstalled(ctx *cli.Context) error { } } + gtprof.EnableBuiltinTracer(util.Iif(setting.IsProd, 2000*time.Millisecond, 100*time.Millisecond)) + // Set up Chi routes webRoutes := routers.NormalRoutes() err := listen(webRoutes, true) diff --git a/flake.lock b/flake.lock index 1890b82dcf..4319737c26 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1726560853, - "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1731139594, - "narHash": "sha256-IigrKK3vYRpUu+HEjPL/phrfh7Ox881er1UEsZvw9Q4=", + "lastModified": 1736798957, + "narHash": "sha256-qwpCtZhSsSNQtK4xYGzMiyEDhkNzOCz/Vfu4oL2ETsQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "76612b17c0ce71689921ca12d9ffdc9c23ce40b2", + "rev": "9abb87b552b7f55ac8916b6fc9e5cb486656a2f3", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index e3655b627e..f54eba1c3b 100644 --- a/flake.nix +++ b/flake.nix @@ -29,9 +29,14 @@ poetry # backend + go_1_23 gofumpt sqlite ]; + shellHook = '' + export GO="${pkgs.go_1_23}/bin/go" + export GOROOT="${pkgs.go_1_23}/share/go" + ''; }; } ); diff --git a/models/db/engine_hook.go b/models/db/engine_hook.go index b4c543c3dd..2c9fc09c99 100644 --- a/models/db/engine_hook.go +++ b/models/db/engine_hook.go @@ -7,23 +7,36 @@ import ( "context" "time" + "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" "xorm.io/xorm/contexts" ) -type SlowQueryHook struct { +type EngineHook struct { Threshold time.Duration Logger log.Logger } -var _ contexts.Hook = (*SlowQueryHook)(nil) +var _ contexts.Hook = (*EngineHook)(nil) -func (*SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { - return c.Ctx, nil +func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { + ctx, _ := gtprof.GetTracer().Start(c.Ctx, gtprof.TraceSpanDatabase) + return ctx, nil } -func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error { +func (h *EngineHook) AfterProcess(c *contexts.ContextHook) error { + span := gtprof.GetContextSpan(c.Ctx) + if span != nil { + // Do not record SQL parameters here: + // * It shouldn't expose the parameters because they contain sensitive information, end users need to report the trace details safely. + // * Some parameters contain quite long texts, waste memory and are difficult to display. + span.SetAttributeString(gtprof.TraceAttrDbSQL, c.SQL) + span.End() + } else { + setting.PanicInDevOrTesting("span in database engine hook is nil") + } if c.ExecuteTime >= h.Threshold { // 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function // is being displayed (the function that ultimately wants to execute the query in the code) diff --git a/models/db/engine_init.go b/models/db/engine_init.go index da85018957..edca697934 100644 --- a/models/db/engine_init.go +++ b/models/db/engine_init.go @@ -72,7 +72,7 @@ func InitEngine(ctx context.Context) error { xe.SetDefaultContext(ctx) if setting.Database.SlowQueryThreshold > 0 { - xe.AddHook(&SlowQueryHook{ + xe.AddHook(&EngineHook{ Threshold: setting.Database.SlowQueryThreshold, Logger: log.GetLogger("xorm"), }) diff --git a/models/fixtures/access.yml b/models/fixtures/access.yml index 4171e31fef..596046e950 100644 --- a/models/fixtures/access.yml +++ b/models/fixtures/access.yml @@ -171,3 +171,9 @@ user_id: 40 repo_id: 61 mode: 4 + +- + id: 30 + user_id: 40 + repo_id: 1 + mode: 2 diff --git a/models/git/branch.go b/models/git/branch.go index e683ce47e6..d1caa35947 100644 --- a/models/git/branch.go +++ b/models/git/branch.go @@ -167,6 +167,9 @@ func GetBranch(ctx context.Context, repoID int64, branchName string) (*Branch, e BranchName: branchName, } } + // FIXME: this design is not right: it doesn't check `branch.IsDeleted`, it doesn't make sense to make callers to check IsDeleted again and again. + // It causes inconsistency with `GetBranches` and `git.GetBranch`, and will lead to strange bugs + // In the future, there should be 2 functions: `GetBranchExisting` and `GetBranchWithDeleted` return &branch, nil } @@ -440,6 +443,8 @@ type FindRecentlyPushedNewBranchesOptions struct { } type RecentlyPushedNewBranch struct { + BranchRepo *repo_model.Repository + BranchName string BranchDisplayName string BranchLink string BranchCompareURL string @@ -540,7 +545,9 @@ func FindRecentlyPushedNewBranches(ctx context.Context, doer *user_model.User, o branchDisplayName = fmt.Sprintf("%s:%s", branch.Repo.FullName(), branchDisplayName) } newBranches = append(newBranches, &RecentlyPushedNewBranch{ + BranchRepo: branch.Repo, BranchDisplayName: branchDisplayName, + BranchName: branch.Name, BranchLink: fmt.Sprintf("%s/src/branch/%s", branch.Repo.Link(), util.PathEscapeSegments(branch.Name)), BranchCompareURL: branch.Repo.ComposeBranchCompareURL(opts.BaseRepo, branch.Name), CommitTime: branch.CommitTime, diff --git a/models/issues/stopwatch.go b/models/issues/stopwatch.go index 629af95b57..7c05a3a883 100644 --- a/models/issues/stopwatch.go +++ b/models/issues/stopwatch.go @@ -46,11 +46,6 @@ func (s Stopwatch) Seconds() int64 { return int64(timeutil.TimeStampNow() - s.CreatedUnix) } -// Duration returns a human-readable duration string based on local server time -func (s Stopwatch) Duration() string { - return util.SecToTime(s.Seconds()) -} - func getStopwatch(ctx context.Context, userID, issueID int64) (sw *Stopwatch, exists bool, err error) { sw = new(Stopwatch) exists, err = db.GetEngine(ctx). @@ -201,7 +196,7 @@ func FinishIssueStopwatch(ctx context.Context, user *user_model.User, issue *Iss Doer: user, Issue: issue, Repo: issue.Repo, - Content: util.SecToTime(timediff), + Content: util.SecToHours(timediff), Type: CommentTypeStopTracking, TimeID: tt.ID, }); err != nil { diff --git a/modules/git/command.go b/modules/git/command.go index 2584e3cc57..602d00f027 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -18,6 +18,7 @@ import ( "time" "code.gitea.io/gitea/modules/git/internal" //nolint:depguard // only this file can use the internal type CmdArg, other files and packages should use AddXxx functions + "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/util" @@ -54,7 +55,7 @@ func logArgSanitize(arg string) string { } else if filepath.IsAbs(arg) { base := filepath.Base(arg) dir := filepath.Dir(arg) - return filepath.Join(filepath.Base(dir), base) + return ".../" + filepath.Join(filepath.Base(dir), base) } return arg } @@ -295,15 +296,20 @@ func (c *Command) run(skip int, opts *RunOpts) error { timeout = defaultCommandExecutionTimeout } - var desc string + cmdLogString := c.LogString() callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */) if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 { callerInfo = callerInfo[pos+1:] } // these logs are for debugging purposes only, so no guarantee of correctness or stability - desc = fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), c.LogString()) + desc := fmt.Sprintf("git.Run(by:%s, repo:%s): %s", callerInfo, logArgSanitize(opts.Dir), cmdLogString) log.Debug("git.Command: %s", desc) + _, span := gtprof.GetTracer().Start(c.parentContext, gtprof.TraceSpanGitRun) + defer span.End() + span.SetAttributeString(gtprof.TraceAttrFuncCaller, callerInfo) + span.SetAttributeString(gtprof.TraceAttrGitCommand, cmdLogString) + var ctx context.Context var cancel context.CancelFunc var finished context.CancelFunc diff --git a/modules/git/command_test.go b/modules/git/command_test.go index 0823afd7f7..e988714db7 100644 --- a/modules/git/command_test.go +++ b/modules/git/command_test.go @@ -58,5 +58,5 @@ func TestCommandString(t *testing.T) { assert.EqualValues(t, cmd.prog+` a "-m msg" "it's a test" "say \"hello\""`, cmd.LogString()) cmd = NewCommandContextNoGlobals(context.Background(), "url: https://a:b@c/", "/root/dir-a/dir-b") - assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" dir-a/dir-b`, cmd.LogString()) + assert.EqualValues(t, cmd.prog+` "url: https://sanitized-credential@c/" .../dir-a/dir-b`, cmd.LogString()) } diff --git a/modules/git/diff.go b/modules/git/diff.go index 833f6220f9..da0a2f26ba 100644 --- a/modules/git/diff.go +++ b/modules/git/diff.go @@ -64,7 +64,10 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff } else if commit.ParentCount() == 0 { cmd.AddArguments("show").AddDynamicArguments(endCommit).AddDashesAndList(files...) } else { - c, _ := commit.Parent(0) + c, err := commit.Parent(0) + if err != nil { + return err + } cmd.AddArguments("diff", "-M").AddDynamicArguments(c.ID.String(), endCommit).AddDashesAndList(files...) } case RawDiffPatch: @@ -74,7 +77,10 @@ func GetRepoRawDiffForFile(repo *Repository, startCommit, endCommit string, diff } else if commit.ParentCount() == 0 { cmd.AddArguments("format-patch", "--no-signature", "--stdout", "--root").AddDynamicArguments(endCommit).AddDashesAndList(files...) } else { - c, _ := commit.Parent(0) + c, err := commit.Parent(0) + if err != nil { + return err + } query := fmt.Sprintf("%s...%s", endCommit, c.ID.String()) cmd.AddArguments("format-patch", "--no-signature", "--stdout").AddDynamicArguments(query).AddDashesAndList(files...) } diff --git a/modules/git/repo_branch_gogit.go b/modules/git/repo_branch_gogit.go index dbc4a5fedc..77aecb21eb 100644 --- a/modules/git/repo_branch_gogit.go +++ b/modules/git/repo_branch_gogit.go @@ -57,7 +57,7 @@ func (repo *Repository) IsBranchExist(name string) bool { // GetBranches returns branches from the repository, skipping "skip" initial branches and // returning at most "limit" branches, or all branches if "limit" is 0. -// Branches are returned with sort of `-commiterdate` as the nogogit +// Branches are returned with sort of `-committerdate` as the nogogit // implementation. This requires full fetch, sort and then the // skip/limit applies later as gogit returns in undefined order. func (repo *Repository) GetBranchNames(skip, limit int) ([]string, int, error) { diff --git a/modules/gtprof/event.go b/modules/gtprof/event.go new file mode 100644 index 0000000000..da4a0faff9 --- /dev/null +++ b/modules/gtprof/event.go @@ -0,0 +1,32 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gtprof + +type EventConfig struct { + attributes []*TraceAttribute +} + +type EventOption interface { + applyEvent(*EventConfig) +} + +type applyEventFunc func(*EventConfig) + +func (f applyEventFunc) applyEvent(cfg *EventConfig) { + f(cfg) +} + +func WithAttributes(attrs ...*TraceAttribute) EventOption { + return applyEventFunc(func(cfg *EventConfig) { + cfg.attributes = append(cfg.attributes, attrs...) + }) +} + +func eventConfigFromOptions(options ...EventOption) *EventConfig { + cfg := &EventConfig{} + for _, opt := range options { + opt.applyEvent(cfg) + } + return cfg +} diff --git a/modules/gtprof/trace.go b/modules/gtprof/trace.go new file mode 100644 index 0000000000..ad67c226dc --- /dev/null +++ b/modules/gtprof/trace.go @@ -0,0 +1,175 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gtprof + +import ( + "context" + "fmt" + "sync" + "time" + + "code.gitea.io/gitea/modules/util" +) + +type contextKey struct { + name string +} + +var contextKeySpan = &contextKey{"span"} + +type traceStarter interface { + start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) +} + +type traceSpanInternal interface { + addEvent(name string, cfg *EventConfig) + recordError(err error, cfg *EventConfig) + end() +} + +type TraceSpan struct { + // immutable + parent *TraceSpan + internalSpans []traceSpanInternal + internalContexts []context.Context + + // mutable, must be protected by mutex + mu sync.RWMutex + name string + statusCode uint32 + statusDesc string + startTime time.Time + endTime time.Time + attributes []*TraceAttribute + children []*TraceSpan +} + +type TraceAttribute struct { + Key string + Value TraceValue +} + +type TraceValue struct { + v any +} + +func (t *TraceValue) AsString() string { + return fmt.Sprint(t.v) +} + +func (t *TraceValue) AsInt64() int64 { + v, _ := util.ToInt64(t.v) + return v +} + +func (t *TraceValue) AsFloat64() float64 { + v, _ := util.ToFloat64(t.v) + return v +} + +var globalTraceStarters []traceStarter + +type Tracer struct { + starters []traceStarter +} + +func (s *TraceSpan) SetName(name string) { + s.mu.Lock() + defer s.mu.Unlock() + s.name = name +} + +func (s *TraceSpan) SetStatus(code uint32, desc string) { + s.mu.Lock() + defer s.mu.Unlock() + s.statusCode, s.statusDesc = code, desc +} + +func (s *TraceSpan) AddEvent(name string, options ...EventOption) { + cfg := eventConfigFromOptions(options...) + for _, tsp := range s.internalSpans { + tsp.addEvent(name, cfg) + } +} + +func (s *TraceSpan) RecordError(err error, options ...EventOption) { + cfg := eventConfigFromOptions(options...) + for _, tsp := range s.internalSpans { + tsp.recordError(err, cfg) + } +} + +func (s *TraceSpan) SetAttributeString(key, value string) *TraceSpan { + s.mu.Lock() + defer s.mu.Unlock() + + s.attributes = append(s.attributes, &TraceAttribute{Key: key, Value: TraceValue{v: value}}) + return s +} + +func (t *Tracer) Start(ctx context.Context, spanName string) (context.Context, *TraceSpan) { + starters := t.starters + if starters == nil { + starters = globalTraceStarters + } + ts := &TraceSpan{name: spanName, startTime: time.Now()} + parentSpan := GetContextSpan(ctx) + if parentSpan != nil { + parentSpan.mu.Lock() + parentSpan.children = append(parentSpan.children, ts) + parentSpan.mu.Unlock() + ts.parent = parentSpan + } + + parentCtx := ctx + for internalSpanIdx, tsp := range starters { + var internalSpan traceSpanInternal + if parentSpan != nil { + parentCtx = parentSpan.internalContexts[internalSpanIdx] + } + ctx, internalSpan = tsp.start(parentCtx, ts, internalSpanIdx) + ts.internalContexts = append(ts.internalContexts, ctx) + ts.internalSpans = append(ts.internalSpans, internalSpan) + } + ctx = context.WithValue(ctx, contextKeySpan, ts) + return ctx, ts +} + +type mutableContext interface { + context.Context + SetContextValue(key, value any) + GetContextValue(key any) any +} + +// StartInContext starts a trace span in Gitea's mutable context (usually the web request context). +// Due to the design limitation of Gitea's web framework, it can't use `context.WithValue` to bind a new span into a new context. +// So here we use our "reqctx" framework to achieve the same result: web request context could always see the latest "span". +func (t *Tracer) StartInContext(ctx mutableContext, spanName string) (*TraceSpan, func()) { + curTraceSpan := GetContextSpan(ctx) + _, newTraceSpan := GetTracer().Start(ctx, spanName) + ctx.SetContextValue(contextKeySpan, newTraceSpan) + return newTraceSpan, func() { + newTraceSpan.End() + ctx.SetContextValue(contextKeySpan, curTraceSpan) + } +} + +func (s *TraceSpan) End() { + s.mu.Lock() + s.endTime = time.Now() + s.mu.Unlock() + + for _, tsp := range s.internalSpans { + tsp.end() + } +} + +func GetTracer() *Tracer { + return &Tracer{} +} + +func GetContextSpan(ctx context.Context) *TraceSpan { + ts, _ := ctx.Value(contextKeySpan).(*TraceSpan) + return ts +} diff --git a/modules/gtprof/trace_builtin.go b/modules/gtprof/trace_builtin.go new file mode 100644 index 0000000000..41743a25e4 --- /dev/null +++ b/modules/gtprof/trace_builtin.go @@ -0,0 +1,96 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gtprof + +import ( + "context" + "fmt" + "strings" + "sync/atomic" + "time" + + "code.gitea.io/gitea/modules/tailmsg" +) + +type traceBuiltinStarter struct{} + +type traceBuiltinSpan struct { + ts *TraceSpan + + internalSpanIdx int +} + +func (t *traceBuiltinSpan) addEvent(name string, cfg *EventConfig) { + // No-op because builtin tracer doesn't need it. + // In the future we might use it to mark the time point between backend logic and network response. +} + +func (t *traceBuiltinSpan) recordError(err error, cfg *EventConfig) { + // No-op because builtin tracer doesn't need it. + // Actually Gitea doesn't handle err this way in most cases +} + +func (t *traceBuiltinSpan) toString(out *strings.Builder, indent int) { + t.ts.mu.RLock() + defer t.ts.mu.RUnlock() + + out.WriteString(strings.Repeat(" ", indent)) + out.WriteString(t.ts.name) + if t.ts.endTime.IsZero() { + out.WriteString(" duration: (not ended)") + } else { + out.WriteString(fmt.Sprintf(" duration=%.4fs", t.ts.endTime.Sub(t.ts.startTime).Seconds())) + } + for _, a := range t.ts.attributes { + out.WriteString(" ") + out.WriteString(a.Key) + out.WriteString("=") + value := a.Value.AsString() + if strings.ContainsAny(value, " \t\r\n") { + quoted := false + for _, c := range "\"'`" { + if quoted = !strings.Contains(value, string(c)); quoted { + value = string(c) + value + string(c) + break + } + } + if !quoted { + value = fmt.Sprintf("%q", value) + } + } + out.WriteString(value) + } + out.WriteString("\n") + for _, c := range t.ts.children { + span := c.internalSpans[t.internalSpanIdx].(*traceBuiltinSpan) + span.toString(out, indent+2) + } +} + +func (t *traceBuiltinSpan) end() { + if t.ts.parent == nil { + // TODO: debug purpose only + // TODO: it should distinguish between http response network lag and actual processing time + threshold := time.Duration(traceBuiltinThreshold.Load()) + if threshold != 0 && t.ts.endTime.Sub(t.ts.startTime) > threshold { + sb := &strings.Builder{} + t.toString(sb, 0) + tailmsg.GetManager().GetTraceRecorder().Record(sb.String()) + } + } +} + +func (t *traceBuiltinStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) { + return ctx, &traceBuiltinSpan{ts: traceSpan, internalSpanIdx: internalSpanIdx} +} + +func init() { + globalTraceStarters = append(globalTraceStarters, &traceBuiltinStarter{}) +} + +var traceBuiltinThreshold atomic.Int64 + +func EnableBuiltinTracer(threshold time.Duration) { + traceBuiltinThreshold.Store(int64(threshold)) +} diff --git a/modules/gtprof/trace_const.go b/modules/gtprof/trace_const.go new file mode 100644 index 0000000000..af9ce9223f --- /dev/null +++ b/modules/gtprof/trace_const.go @@ -0,0 +1,19 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gtprof + +// Some interesting names could be found in https://github.com/open-telemetry/opentelemetry-go/tree/main/semconv + +const ( + TraceSpanHTTP = "http" + TraceSpanGitRun = "git-run" + TraceSpanDatabase = "database" +) + +const ( + TraceAttrFuncCaller = "func.caller" + TraceAttrDbSQL = "db.sql" + TraceAttrGitCommand = "git.command" + TraceAttrHTTPRoute = "http.route" +) diff --git a/modules/gtprof/trace_test.go b/modules/gtprof/trace_test.go new file mode 100644 index 0000000000..7e1743c88d --- /dev/null +++ b/modules/gtprof/trace_test.go @@ -0,0 +1,93 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package gtprof + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" +) + +// "vendor span" is a simple demo for a span from a vendor library + +var vendorContextKey any = "vendorContextKey" + +type vendorSpan struct { + name string + children []*vendorSpan +} + +func vendorTraceStart(ctx context.Context, name string) (context.Context, *vendorSpan) { + span := &vendorSpan{name: name} + parentSpan, ok := ctx.Value(vendorContextKey).(*vendorSpan) + if ok { + parentSpan.children = append(parentSpan.children, span) + } + ctx = context.WithValue(ctx, vendorContextKey, span) + return ctx, span +} + +// below "testTrace*" integrate the vendor span into our trace system + +type testTraceSpan struct { + vendorSpan *vendorSpan +} + +func (t *testTraceSpan) addEvent(name string, cfg *EventConfig) {} + +func (t *testTraceSpan) recordError(err error, cfg *EventConfig) {} + +func (t *testTraceSpan) end() {} + +type testTraceStarter struct{} + +func (t *testTraceStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) { + ctx, span := vendorTraceStart(ctx, traceSpan.name) + return ctx, &testTraceSpan{span} +} + +func TestTraceStarter(t *testing.T) { + globalTraceStarters = []traceStarter{&testTraceStarter{}} + + ctx := context.Background() + ctx, span := GetTracer().Start(ctx, "root") + defer span.End() + + func(ctx context.Context) { + ctx, span := GetTracer().Start(ctx, "span1") + defer span.End() + func(ctx context.Context) { + _, span := GetTracer().Start(ctx, "spanA") + defer span.End() + }(ctx) + func(ctx context.Context) { + _, span := GetTracer().Start(ctx, "spanB") + defer span.End() + }(ctx) + }(ctx) + + func(ctx context.Context) { + _, span := GetTracer().Start(ctx, "span2") + defer span.End() + }(ctx) + + var spanFullNames []string + var collectSpanNames func(parentFullName string, s *vendorSpan) + collectSpanNames = func(parentFullName string, s *vendorSpan) { + fullName := parentFullName + "/" + s.name + spanFullNames = append(spanFullNames, fullName) + for _, c := range s.children { + collectSpanNames(fullName, c) + } + } + collectSpanNames("", span.internalSpans[0].(*testTraceSpan).vendorSpan) + assert.Equal(t, []string{ + "/root", + "/root/span1", + "/root/span1/spanA", + "/root/span1/spanB", + "/root/span2", + }, spanFullNames) +} diff --git a/modules/repository/branch.go b/modules/repository/branch.go index 4630e70aa8..2bf9930f19 100644 --- a/modules/repository/branch.go +++ b/modules/repository/branch.go @@ -6,7 +6,6 @@ package repository import ( "context" "fmt" - "strings" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" @@ -52,9 +51,6 @@ func SyncRepoBranchesWithRepo(ctx context.Context, repo *repo_model.Repository, { branches, _, err := gitRepo.GetBranchNames(0, 0) if err != nil { - if strings.Contains(err.Error(), "ref file is empty") { - return 0, nil - } return 0, err } log.Trace("SyncRepoBranches[%s]: branches[%d]: %v", repo.FullName(), len(branches), branches) diff --git a/modules/tailmsg/talimsg.go b/modules/tailmsg/talimsg.go new file mode 100644 index 0000000000..aafc98e2d2 --- /dev/null +++ b/modules/tailmsg/talimsg.go @@ -0,0 +1,73 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package tailmsg + +import ( + "sync" + "time" +) + +type MsgRecord struct { + Time time.Time + Content string +} + +type MsgRecorder interface { + Record(content string) + GetRecords() []*MsgRecord +} + +type memoryMsgRecorder struct { + mu sync.RWMutex + msgs []*MsgRecord + limit int +} + +// TODO: use redis for a clustered environment + +func (m *memoryMsgRecorder) Record(content string) { + m.mu.Lock() + defer m.mu.Unlock() + m.msgs = append(m.msgs, &MsgRecord{ + Time: time.Now(), + Content: content, + }) + if len(m.msgs) > m.limit { + m.msgs = m.msgs[len(m.msgs)-m.limit:] + } +} + +func (m *memoryMsgRecorder) GetRecords() []*MsgRecord { + m.mu.RLock() + defer m.mu.RUnlock() + ret := make([]*MsgRecord, len(m.msgs)) + copy(ret, m.msgs) + return ret +} + +func NewMsgRecorder(limit int) MsgRecorder { + return &memoryMsgRecorder{ + limit: limit, + } +} + +type Manager struct { + traceRecorder MsgRecorder + logRecorder MsgRecorder +} + +func (m *Manager) GetTraceRecorder() MsgRecorder { + return m.traceRecorder +} + +func (m *Manager) GetLogRecorder() MsgRecorder { + return m.logRecorder +} + +var GetManager = sync.OnceValue(func() *Manager { + return &Manager{ + traceRecorder: NewMsgRecorder(100), + logRecorder: NewMsgRecorder(1000), + } +}) diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 609407d36b..a2cc166de9 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -69,7 +69,7 @@ func NewFuncMap() template.FuncMap { // time / number / format "FileSize": base.FileSize, "CountFmt": countFmt, - "Sec2Time": util.SecToTime, + "Sec2Time": util.SecToHours, "TimeEstimateString": timeEstimateString, diff --git a/modules/util/sec_to_time.go b/modules/util/sec_to_time.go index ad0fb1a68b..73667d723e 100644 --- a/modules/util/sec_to_time.go +++ b/modules/util/sec_to_time.go @@ -8,59 +8,17 @@ import ( "strings" ) -// SecToTime converts an amount of seconds to a human-readable string. E.g. -// 66s -> 1 minute 6 seconds -// 52410s -> 14 hours 33 minutes -// 563418 -> 6 days 12 hours -// 1563418 -> 2 weeks 4 days -// 3937125s -> 1 month 2 weeks -// 45677465s -> 1 year 6 months -func SecToTime(durationVal any) string { +// SecToHours converts an amount of seconds to a human-readable hours string. +// This is stable for planning and managing timesheets. +// Here it only supports hours and minutes, because a work day could contain 6 or 7 or 8 hours. +func SecToHours(durationVal any) string { duration, _ := ToInt64(durationVal) + hours := duration / 3600 + minutes := (duration / 60) % 60 formattedTime := "" - - // The following four variables are calculated by taking - // into account the previously calculated variables, this avoids - // pitfalls when using remainders. As that could lead to incorrect - // results when the calculated number equals the quotient number. - remainingDays := duration / (60 * 60 * 24) - years := remainingDays / 365 - remainingDays -= years * 365 - months := remainingDays * 12 / 365 - remainingDays -= months * 365 / 12 - weeks := remainingDays / 7 - remainingDays -= weeks * 7 - days := remainingDays - - // The following three variables are calculated without depending - // on the previous calculated variables. - hours := (duration / 3600) % 24 - minutes := (duration / 60) % 60 - seconds := duration % 60 - - // Extract only the relevant information of the time - // If the time is greater than a year, it makes no sense to display seconds. - switch { - case years > 0: - formattedTime = formatTime(years, "year", formattedTime) - formattedTime = formatTime(months, "month", formattedTime) - case months > 0: - formattedTime = formatTime(months, "month", formattedTime) - formattedTime = formatTime(weeks, "week", formattedTime) - case weeks > 0: - formattedTime = formatTime(weeks, "week", formattedTime) - formattedTime = formatTime(days, "day", formattedTime) - case days > 0: - formattedTime = formatTime(days, "day", formattedTime) - formattedTime = formatTime(hours, "hour", formattedTime) - case hours > 0: - formattedTime = formatTime(hours, "hour", formattedTime) - formattedTime = formatTime(minutes, "minute", formattedTime) - default: - formattedTime = formatTime(minutes, "minute", formattedTime) - formattedTime = formatTime(seconds, "second", formattedTime) - } + formattedTime = formatTime(hours, "hour", formattedTime) + formattedTime = formatTime(minutes, "minute", formattedTime) // The formatTime() function always appends a space at the end. This will be trimmed return strings.TrimRight(formattedTime, " ") @@ -76,6 +34,5 @@ func formatTime(value int64, name, formattedTime string) string { } else if value > 1 { formattedTime = fmt.Sprintf("%s%d %ss ", formattedTime, value, name) } - return formattedTime } diff --git a/modules/util/sec_to_time_test.go b/modules/util/sec_to_time_test.go index 4d1213a52c..71a8801d4f 100644 --- a/modules/util/sec_to_time_test.go +++ b/modules/util/sec_to_time_test.go @@ -9,22 +9,17 @@ import ( "github.com/stretchr/testify/assert" ) -func TestSecToTime(t *testing.T) { +func TestSecToHours(t *testing.T) { second := int64(1) minute := 60 * second hour := 60 * minute day := 24 * hour - year := 365 * day - assert.Equal(t, "1 minute 6 seconds", SecToTime(minute+6*second)) - assert.Equal(t, "1 hour", SecToTime(hour)) - assert.Equal(t, "1 hour", SecToTime(hour+second)) - assert.Equal(t, "14 hours 33 minutes", SecToTime(14*hour+33*minute+30*second)) - assert.Equal(t, "6 days 12 hours", SecToTime(6*day+12*hour+30*minute+18*second)) - assert.Equal(t, "2 weeks 4 days", SecToTime((2*7+4)*day+2*hour+16*minute+58*second)) - assert.Equal(t, "4 weeks", SecToTime(4*7*day)) - assert.Equal(t, "4 weeks 1 day", SecToTime((4*7+1)*day)) - assert.Equal(t, "1 month 2 weeks", SecToTime((6*7+3)*day+13*hour+38*minute+45*second)) - assert.Equal(t, "11 months", SecToTime(year-25*day)) - assert.Equal(t, "1 year 5 months", SecToTime(year+163*day+10*hour+11*minute+5*second)) + assert.Equal(t, "1 minute", SecToHours(minute+6*second)) + assert.Equal(t, "1 hour", SecToHours(hour)) + assert.Equal(t, "1 hour", SecToHours(hour+second)) + assert.Equal(t, "14 hours 33 minutes", SecToHours(14*hour+33*minute+30*second)) + assert.Equal(t, "156 hours 30 minutes", SecToHours(6*day+12*hour+30*minute+18*second)) + assert.Equal(t, "98 hours 16 minutes", SecToHours(4*day+2*hour+16*minute+58*second)) + assert.Equal(t, "672 hours", SecToHours(4*7*day)) } diff --git a/modules/web/handler.go b/modules/web/handler.go index 9a3e4a7f17..42a649714d 100644 --- a/modules/web/handler.go +++ b/modules/web/handler.go @@ -121,7 +121,7 @@ func wrapHandlerProvider[T http.Handler](hp func(next http.Handler) T, funcInfo return func(next http.Handler) http.Handler { h := hp(next) // this handle could be dynamically generated, so we can't use it for debug info return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - routing.UpdateFuncInfo(req.Context(), funcInfo) + defer routing.RecordFuncInfo(req.Context(), funcInfo)() h.ServeHTTP(resp, req) }) } @@ -157,7 +157,7 @@ func toHandlerProvider(handler any) func(next http.Handler) http.Handler { return // it's doing pre-check, just return } - routing.UpdateFuncInfo(req.Context(), funcInfo) + defer routing.RecordFuncInfo(req.Context(), funcInfo)() ret := fn.Call(argsIn) // handle the return value (no-op at the moment) diff --git a/modules/web/routing/context.go b/modules/web/routing/context.go index c5e85a415b..d3eb98f83d 100644 --- a/modules/web/routing/context.go +++ b/modules/web/routing/context.go @@ -6,22 +6,29 @@ package routing import ( "context" "net/http" + + "code.gitea.io/gitea/modules/gtprof" + "code.gitea.io/gitea/modules/reqctx" ) type contextKeyType struct{} var contextKey contextKeyType -// UpdateFuncInfo updates a context's func info -func UpdateFuncInfo(ctx context.Context, funcInfo *FuncInfo) { - record, ok := ctx.Value(contextKey).(*requestRecord) - if !ok { - return +// RecordFuncInfo records a func info into context +func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) { + end = func() {} + if reqCtx := reqctx.FromContext(ctx); reqCtx != nil { + var traceSpan *gtprof.TraceSpan + traceSpan, end = gtprof.GetTracer().StartInContext(reqCtx, "http.func") + traceSpan.SetAttributeString("func", funcInfo.shortName) } - - record.lock.Lock() - record.funcInfo = funcInfo - record.lock.Unlock() + if record, ok := ctx.Value(contextKey).(*requestRecord); ok { + record.lock.Lock() + record.funcInfo = funcInfo + record.lock.Unlock() + } + return end } // MarkLongPolling marks the request is a long-polling request, and the logger may output different message for it diff --git a/options/gitignore/Node b/options/gitignore/Node index c6bba59138..1170717c14 100644 --- a/options/gitignore/Node +++ b/options/gitignore/Node @@ -104,6 +104,12 @@ dist .temp .cache +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + # Docusaurus cache and generated files .docusaurus diff --git a/options/gitignore/Python b/options/gitignore/Python index 15201acc11..0a197900e2 100644 --- a/options/gitignore/Python +++ b/options/gitignore/Python @@ -167,5 +167,8 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +# Ruff stuff: +.ruff_cache/ + # PyPI configuration file .pypirc diff --git a/options/gitignore/Rust b/options/gitignore/Rust index d01bd1a990..0104787a73 100644 --- a/options/gitignore/Rust +++ b/options/gitignore/Rust @@ -3,10 +3,6 @@ debug/ target/ -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - # These are backup files generated by rustfmt **/*.rs.bk diff --git a/options/locale/locale_cs-CZ.ini b/options/locale/locale_cs-CZ.ini index 78e8db160c..7bbc509c23 100644 --- a/options/locale/locale_cs-CZ.ini +++ b/options/locale/locale_cs-CZ.ini @@ -1683,16 +1683,13 @@ issues.timetracker_timer_manually_add=Přidat čas issues.time_estimate_set=Nastavit odhadovaný čas issues.time_estimate_display=Odhad: %s -issues.change_time_estimate_at=změnil/a odhad času na <b>%s</b> %s issues.remove_time_estimate_at=odstranil/a odhad času %s issues.time_estimate_invalid=Formát odhadu času je neplatný issues.start_tracking_history=započal/a práci %s issues.tracker_auto_close=Časovač se automaticky zastaví po zavření tohoto úkolu issues.tracking_already_started=`Již jste spustili sledování času na <a href="%s">jiném úkolu</a>!` -issues.stop_tracking_history=pracoval/a <b>%s</b> %s issues.cancel_tracking_history=`zrušil/a sledování času %s` issues.del_time=Odstranit tento časový záznam -issues.add_time_history=přidal/a strávený čas <b>%s</b> %s issues.del_time_history=`odstranil/a strávený čas %s` issues.add_time_manually=Přidat čas ručně issues.add_time_hours=Hodiny @@ -3369,7 +3366,6 @@ monitor.execute_time=Doba provádění monitor.last_execution_result=Výsledek monitor.process.cancel=Zrušit proces monitor.process.cancel_desc=Zrušení procesu může způsobit ztrátu dat -monitor.process.cancel_notices=Zrušit: <strong>%s</strong>? monitor.process.children=Potomek monitor.queues=Fronty @@ -3566,7 +3562,6 @@ conda.install=Pro instalaci balíčku pomocí Conda spusťte následující př container.details.type=Typ obrazu container.details.platform=Platforma container.pull=Stáhněte obraz z příkazové řádky: -container.digest=Výběr: container.multi_arch=OS/architektura container.layers=Vrstvy obrazů container.labels=Štítky diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 122bc1ec53..8fd2a7eda0 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -1678,16 +1678,13 @@ issues.timetracker_timer_manually_add=Zeit hinzufügen issues.time_estimate_set=Geschätzte Zeit festlegen issues.time_estimate_display=Schätzung: %s -issues.change_time_estimate_at=Zeitschätzung geändert zu <b>%s</b> %s issues.remove_time_estimate_at=Zeitschätzung %s entfernt issues.time_estimate_invalid=Format der Zeitschätzung ist ungültig issues.start_tracking_history=hat die Zeiterfassung %s gestartet issues.tracker_auto_close=Der Timer wird automatisch gestoppt, wenn dieser Issue geschlossen wird issues.tracking_already_started=`Du hast die Zeiterfassung bereits in <a href="%s">diesem Issue</a> gestartet!` -issues.stop_tracking_history=hat für <b>%s</b> gearbeitet %s issues.cancel_tracking_history=`hat die Zeiterfassung %s abgebrochen` issues.del_time=Diese Zeiterfassung löschen -issues.add_time_history=hat <b>%s</b> gearbeitete Zeit hinzugefügt %s issues.del_time_history=`hat %s gearbeitete Zeit gelöscht` issues.add_time_manually=Zeit manuell hinzufügen issues.add_time_hours=Stunden @@ -3359,7 +3356,6 @@ monitor.execute_time=Ausführungszeit monitor.last_execution_result=Ergebnis monitor.process.cancel=Prozess abbrechen monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen -monitor.process.cancel_notices=Abbrechen: <strong>%s</strong>? monitor.process.children=Subprozesse monitor.queues=Warteschlangen @@ -3555,7 +3551,6 @@ conda.install=Um das Paket mit Conda zu installieren, führe den folgenden Befeh container.details.type=Container-Image Typ container.details.platform=Plattform container.pull=Downloade das Container-Image aus der Kommandozeile: -container.digest=Digest: container.multi_arch=Betriebsystem / Architektur container.layers=Container-Image Ebenen container.labels=Labels diff --git a/options/locale/locale_el-GR.ini b/options/locale/locale_el-GR.ini index af26256314..e989819c5e 100644 --- a/options/locale/locale_el-GR.ini +++ b/options/locale/locale_el-GR.ini @@ -3236,7 +3236,6 @@ conda.install=Για να εγκαταστήσετε το πακέτο χρησ container.details.type=Τύπος Εικόνας container.details.platform=Πλατφόρμα container.pull=Κατεβάστε την εικόνα από τη γραμμή εντολών: -container.digest=Σύνοψη: container.multi_arch=ΛΣ / Αρχιτεκτονική container.layers=Στρώματα Εικόνας container.labels=Ετικέτες diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 0adc3193a0..ea39ddf837 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1690,16 +1690,16 @@ issues.timetracker_timer_manually_add = Add Time issues.time_estimate_set = Set estimated time issues.time_estimate_display = Estimate: %s -issues.change_time_estimate_at = changed time estimate to <b>%s</b> %s +issues.change_time_estimate_at = changed time estimate to <b>%[1]s</b> %[2]s issues.remove_time_estimate_at = removed time estimate %s issues.time_estimate_invalid = Time estimate format is invalid issues.start_tracking_history = started working %s issues.tracker_auto_close = Timer will be stopped automatically when this issue gets closed issues.tracking_already_started = `You have already started time tracking on <a href="%s">another issue</a>!` -issues.stop_tracking_history = worked for <b>%s</b> %s +issues.stop_tracking_history = worked for <b>%[1]s</b> %[2]s issues.cancel_tracking_history = `canceled time tracking %s` issues.del_time = Delete this time log -issues.add_time_history = added spent time <b>%s</b> %s +issues.add_time_history = added spent time <b>%[1]s</b> %[2]s issues.del_time_history= `deleted spent time %s` issues.add_time_manually = Manually Add Time issues.add_time_hours = Hours @@ -1958,7 +1958,7 @@ pulls.upstream_diverging_prompt_behind_1 = This branch is %[1]d commit behind %[ pulls.upstream_diverging_prompt_behind_n = This branch is %[1]d commits behind %[2]s pulls.upstream_diverging_prompt_base_newer = The base branch %s has new changes pulls.upstream_diverging_merge = Sync fork -pulls.upstream_diverging_merge_confirm = Would you like to merge base repository's default branch onto this repository's branch %s? +pulls.upstream_diverging_merge_confirm = Would you like to merge "%[1]s" onto "%[2]s"? pull.deleted_branch = (deleted):%s pull.agit_documentation = Review documentation about AGit @@ -2719,6 +2719,8 @@ branch.create_branch_operation = Create branch branch.new_branch = Create new branch branch.new_branch_from = Create new branch from "%s" branch.renamed = Branch %s was renamed to %s. +branch.rename_default_or_protected_branch_error = Only admins can rename default or protected branches. +branch.rename_protected_branch_failed = This branch is protected by glob-based protection rules. tag.create_tag = Create tag %s tag.create_tag_operation = Create tag @@ -3396,6 +3398,8 @@ monitor.previous = Previous Time monitor.execute_times = Executions monitor.process = Running Processes monitor.stacktrace = Stacktrace +monitor.trace = Trace +monitor.performance_logs = Performance Logs monitor.processes_count = %d Processes monitor.download_diagnosis_report = Download diagnosis report monitor.desc = Description @@ -3404,7 +3408,6 @@ monitor.execute_time = Execution Time monitor.last_execution_result = Result monitor.process.cancel = Cancel process monitor.process.cancel_desc = Cancelling a process may cause data loss -monitor.process.cancel_notices = Cancel: <strong>%s</strong>? monitor.process.children = Children monitor.queues = Queues @@ -3601,7 +3604,8 @@ conda.install = To install the package using Conda, run the following command: container.details.type = Image Type container.details.platform = Platform container.pull = Pull the image from the command line: -container.digest = Digest: +container.images = Images +container.digest = Digest container.multi_arch = OS / Arch container.layers = Image Layers container.labels = Labels diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index e85e6b3399..049fb9196d 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -3215,7 +3215,6 @@ conda.install=Para instalar el paquete usando Conda, ejecute el siguiente comand container.details.type=Tipo de imagen container.details.platform=Plataforma container.pull=Arrastra la imagen desde la línea de comandos: -container.digest=Resumen: container.multi_arch=SO / Arquitectura container.layers=Capas de imagen container.labels=Etiquetas diff --git a/options/locale/locale_fr-FR.ini b/options/locale/locale_fr-FR.ini index bf31111b6e..0df4f5a00c 100644 --- a/options/locale/locale_fr-FR.ini +++ b/options/locale/locale_fr-FR.ini @@ -1683,16 +1683,13 @@ issues.timetracker_timer_manually_add=Pointer du temps issues.time_estimate_set=Définir le temps estimé issues.time_estimate_display=Estimation : %s -issues.change_time_estimate_at=a changé le temps estimé à <b>%s</b> %s issues.remove_time_estimate_at=a supprimé le temps estimé %s issues.time_estimate_invalid=Le format du temps estimé est invalide issues.start_tracking_history=`a commencé son travail %s.` issues.tracker_auto_close=Le minuteur sera automatiquement arrêté quand le ticket sera fermé. issues.tracking_already_started=`Vous avez déjà un minuteur en cours sur <a href="%s">un autre ticket</a> !` -issues.stop_tracking_history=`a fini de travailler sur <b>%s</b> %s.` issues.cancel_tracking_history=`a abandonné son minuteur %s.` issues.del_time=Supprimer ce minuteur du journal -issues.add_time_history=`a pointé du temps de travail %s.` issues.del_time_history=`a supprimé son temps de travail %s.` issues.add_time_manually=Temps pointé manuellement issues.add_time_hours=Heures @@ -3370,7 +3367,6 @@ monitor.execute_time=Heure d'Éxécution monitor.last_execution_result=Résultat monitor.process.cancel=Annuler le processus monitor.process.cancel_desc=L’annulation d’un processus peut entraîner une perte de données. -monitor.process.cancel_notices=Annuler : <strong>%s</strong> ? monitor.process.children=Enfant monitor.queues=Files d'attente @@ -3567,7 +3563,6 @@ conda.install=Pour installer le paquet en utilisant Conda, exécutez la commande container.details.type=Type d'image container.details.platform=Plateforme container.pull=Tirez l'image depuis un terminal : -container.digest=Empreinte : container.multi_arch=SE / Arch container.layers=Calques d'image container.labels=Labels diff --git a/options/locale/locale_ga-IE.ini b/options/locale/locale_ga-IE.ini index 3acdba35b0..9f63358a32 100644 --- a/options/locale/locale_ga-IE.ini +++ b/options/locale/locale_ga-IE.ini @@ -1684,16 +1684,13 @@ issues.timetracker_timer_manually_add=Cuir Am leis issues.time_estimate_set=Socraigh am measta issues.time_estimate_display=Meastachán: %s -issues.change_time_estimate_at=d'athraigh an meastachán ama go <b>%s</b> %s issues.remove_time_estimate_at=baineadh meastachán ama %s issues.time_estimate_invalid=Tá formáid meastachán ama neamhbhailí issues.start_tracking_history=thosaigh ag obair %s issues.tracker_auto_close=Stopfar ama go huathoibríoch nuair a dhúnfar an tsaincheist seo issues.tracking_already_started=`Tá tús curtha agat cheana féin ag rianú ama ar <a href="%s">eagrán eile</a>!` -issues.stop_tracking_history=d'oibrigh do <b>%s</b> %s issues.cancel_tracking_history=`rianú ama curtha ar ceal %s` issues.del_time=Scrios an log ama seo -issues.add_time_history=cuireadh am caite <b>%s</b> %s leis issues.del_time_history=`an t-am caite scriosta %s` issues.add_time_manually=Cuir Am leis de Láimh issues.add_time_hours=Uaireanta @@ -3371,7 +3368,6 @@ monitor.execute_time=Am Forghníomhaithe monitor.last_execution_result=Toradh monitor.process.cancel=Cealaigh próiseas monitor.process.cancel_desc=Má chuirtear próiseas ar ceal d'fhéadfadh go gcaillfí sonraí -monitor.process.cancel_notices=Cealaigh: <strong>%s</strong>? monitor.process.children=Leanaí monitor.queues=Scuaineanna @@ -3568,7 +3564,6 @@ conda.install=Chun an pacáiste a shuiteáil ag úsáid Conda, reáchtáil an t- container.details.type=Cineál Íomhá container.details.platform=Ardán container.pull=Tarraing an íomhá ón líne ordaithe: -container.digest=Díleáigh: container.multi_arch=Córas Oibriúcháin / Ailtireacht container.layers=Sraitheanna Íomhá container.labels=Lipéid diff --git a/options/locale/locale_ja-JP.ini b/options/locale/locale_ja-JP.ini index efe365dbfa..931e0523f9 100644 --- a/options/locale/locale_ja-JP.ini +++ b/options/locale/locale_ja-JP.ini @@ -1034,6 +1034,8 @@ fork_to_different_account=別のアカウントにフォークする fork_visibility_helper=フォークしたリポジトリの公開/非公開は変更できません。 fork_branch=フォークにクローンされるブランチ all_branches=すべてのブランチ +view_all_branches=すべてのブランチを表示 +view_all_tags=すべてのタグを表示 fork_no_valid_owners=このリポジトリには有効なオーナーがいないため、フォークできません。 fork.blocked_user=リポジトリのオーナーがあなたをブロックしているため、リポジトリをフォークできません。 use_template=このテンプレートを使用 @@ -1108,6 +1110,7 @@ delete_preexisting_success=%s の未登録ファイルを削除しました blame_prior=この変更より前のBlameを表示 blame.ignore_revs=<a href="%s">.git-blame-ignore-revs</a> で指定されたリビジョンは除外しています。 これを迂回して通常のBlame表示を見るには <a href="%s">ここ</a>をクリック。 blame.ignore_revs.failed=<a href="%s">.git-blame-ignore-revs</a> によるリビジョンの無視は失敗しました。 +user_search_tooltip=最大30人までのユーザーを表示 transfer.accept=移転を承認 @@ -1226,6 +1229,7 @@ create_new_repo_command=コマンドラインから新しいリポジトリを push_exist_repo=コマンドラインから既存のリポジトリをプッシュ empty_message=このリポジトリの中には何もありません。 broken_message=このリポジトリの基礎となる Git のデータを読み取れません。このインスタンスの管理者に相談するか、このリポジトリを削除してください。 +no_branch=このリポジトリにはブランチがありません。 code=コード code.desc=ソースコード、ファイル、コミット、ブランチにアクセス。 @@ -1523,6 +1527,8 @@ issues.filter_assignee=担当者 issues.filter_assginee_no_select=すべての担当者 issues.filter_assginee_no_assignee=担当者なし issues.filter_poster=作成者 +issues.filter_user_placeholder=ユーザーを検索 +issues.filter_user_no_select=すべてのユーザー issues.filter_type=タイプ issues.filter_type.all_issues=すべてのイシュー issues.filter_type.assigned_to_you=自分が担当 @@ -1674,16 +1680,16 @@ issues.timetracker_timer_manually_add=時間を追加 issues.time_estimate_set=見積時間を設定 issues.time_estimate_display=見積時間: %s -issues.change_time_estimate_at=が見積時間を <b>%s</b> に変更 %s +issues.change_time_estimate_at=が見積時間を <b>%[1]s</b> に変更 %[2]s issues.remove_time_estimate_at=が見積時間を削除 %s issues.time_estimate_invalid=見積時間のフォーマットが不正です issues.start_tracking_history=が作業を開始 %s issues.tracker_auto_close=タイマーは、このイシューがクローズされると自動的に終了します issues.tracking_already_started=`<a href="%s">別のイシュー</a>で既にタイムトラッキングを開始しています!` -issues.stop_tracking_history=が <b>%s</b> の作業を終了 %s +issues.stop_tracking_history=が <b>%[1]s</b> の作業を終了 %[2]s issues.cancel_tracking_history=`がタイムトラッキングを中止 %s` issues.del_time=このタイムログを削除 -issues.add_time_history=が作業時間 <b>%s</b> を追加 %s +issues.add_time_history=が作業時間 <b>%[1]s</b> を追加 %[2]s issues.del_time_history=`が作業時間を削除 %s` issues.add_time_manually=時間の手入力 issues.add_time_hours=時間 @@ -1938,6 +1944,8 @@ pulls.delete.title=このプルリクエストを削除しますか? pulls.delete.text=本当にこのプルリクエストを削除しますか? (これはすべてのコンテンツを完全に削除します。 保存しておきたい場合は、代わりにクローズすることを検討してください) pulls.recently_pushed_new_branches=%[2]s 、あなたはブランチ <strong>%[1]s</strong> にプッシュしました +pulls.upstream_diverging_prompt_behind_1=このブランチは %[2]s よりも %[1]d コミット遅れています +pulls.upstream_diverging_prompt_behind_n=このブランチは %[2]s よりも %[1]d コミット遅れています pulls.upstream_diverging_prompt_base_newer=ベースブランチ %s に新しい変更があります pulls.upstream_diverging_merge=フォークを同期 @@ -2621,6 +2629,7 @@ release.new_release=新しいリリース release.draft=下書き release.prerelease=プレリリース release.stable=安定版 +release.latest=最新 release.compare=比較 release.edit=編集 release.ahead.commits=<strong>%d</strong>件のコミット @@ -2849,6 +2858,7 @@ teams.invite.title=あなたは組織 <strong>%[2]s</strong> 内のチーム <st teams.invite.by=%s からの招待 teams.invite.description=下のボタンをクリックしてチームに参加してください。 +view_as_role=表示: %s view_as_public_hint=READMEを公開ユーザーとして見ています。 view_as_member_hint=READMEをこの組織のメンバーとして見ています。 @@ -3354,7 +3364,6 @@ monitor.execute_time=実行時間 monitor.last_execution_result=結果 monitor.process.cancel=処理をキャンセル monitor.process.cancel_desc=処理をキャンセルするとデータが失われる可能性があります -monitor.process.cancel_notices=キャンセル: <strong>%s</strong>? monitor.process.children=子プロセス monitor.queues=キュー @@ -3550,7 +3559,7 @@ conda.install=Conda を使用してパッケージをインストールするに container.details.type=イメージタイプ container.details.platform=プラットフォーム container.pull=コマンドラインでイメージを取得します: -container.digest=ダイジェスト: +container.digest=ダイジェスト container.multi_arch=OS / アーキテクチャ container.layers=イメージレイヤー container.labels=ラベル diff --git a/options/locale/locale_lv-LV.ini b/options/locale/locale_lv-LV.ini index 8e1c4a651c..cc2dcd1180 100644 --- a/options/locale/locale_lv-LV.ini +++ b/options/locale/locale_lv-LV.ini @@ -3239,7 +3239,6 @@ conda.install=Lai instalētu Conda pakotni, izpildiet sekojošu komandu: container.details.type=Attēla formāts container.details.platform=Platforma container.pull=Atgādājiet šo attēlu no komandrindas: -container.digest=Īssavilkums: container.multi_arch=OS / arhitektūra container.layers=Attēla slāņi container.labels=Iezīmes diff --git a/options/locale/locale_pl-PL.ini b/options/locale/locale_pl-PL.ini index 4d049c83d1..4dfae86bb6 100644 --- a/options/locale/locale_pl-PL.ini +++ b/options/locale/locale_pl-PL.ini @@ -2310,7 +2310,6 @@ monitor.start=Czas rozpoczęcia monitor.execute_time=Czas wykonania monitor.process.cancel=Anuluj proces monitor.process.cancel_desc=Anulowanie procesu może spowodować utratę danych -monitor.process.cancel_notices=Anuluj: <strong>%s</strong>? monitor.queues=Kolejki monitor.queue=Kolejka: %s diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index f42de9287e..65d6b4569b 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -3180,7 +3180,6 @@ conda.install=Para instalar o pacote usando o Conda, execute o seguinte comando: container.details.type=Tipo de Imagem container.details.platform=Plataforma container.pull=Puxe a imagem pela linha de comando: -container.digest=Digest: container.multi_arch=S.O. / Arquitetura container.layers=Camadas da Imagem container.labels=Rótulos diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 40f1a13d67..270728582a 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -1684,16 +1684,16 @@ issues.timetracker_timer_manually_add=Adicionar tempo issues.time_estimate_set=Definir tempo estimado issues.time_estimate_display=Estimativa: %s -issues.change_time_estimate_at=alterou a estimativa de tempo para <b>%s</b> %s +issues.change_time_estimate_at=alterou a estimativa de tempo para <b>%[1]s</b> %[2]s issues.remove_time_estimate_at=removeu a estimativa de tempo %s issues.time_estimate_invalid=O formato da estimativa de tempo é inválido issues.start_tracking_history=começou a trabalhar %s issues.tracker_auto_close=O cronómetro será parado automaticamente quando esta questão for fechada issues.tracking_already_started=`Você já iniciou a contagem de tempo <a href="%s">noutra questão</a>!` -issues.stop_tracking_history=trabalhou durante <b>%s</b> %s +issues.stop_tracking_history=trabalhou durante <b>%[1]s</b> %[2]s issues.cancel_tracking_history=`cancelou a contagem de tempo %s` issues.del_time=Eliminar este registo de tempo -issues.add_time_history=adicionou <b>%s</b> de tempo gasto %s +issues.add_time_history=adicionou <b>%[1]s</b> de tempo gasto %[2]s issues.del_time_history=`eliminou o tempo gasto nesta questão %s` issues.add_time_manually=Adicionar tempo manualmente issues.add_time_hours=Horas @@ -2157,6 +2157,7 @@ settings.advanced_settings=Configurações avançadas settings.wiki_desc=Habilitar wiki do repositório settings.use_internal_wiki=Usar o wiki integrado settings.default_wiki_branch_name=Nome do ramo predefinido do wiki +settings.default_permission_everyone_access=Permissão de acesso predefinida para todos os utilizadores registados: settings.failed_to_change_default_wiki_branch=Falhou ao mudar o nome do ramo predefinido do wiki. settings.use_external_wiki=Usar um wiki externo settings.external_wiki_url=URL do wiki externo @@ -2711,6 +2712,8 @@ branch.create_branch_operation=Criar ramo branch.new_branch=Criar um novo ramo branch.new_branch_from=`Criar um novo ramo a partir do ramo "%s"` branch.renamed=O ramo %s foi renomeado para %s. +branch.rename_default_or_protected_branch_error=Só os administradores é que podem renomear o ramo principal ou ramos protegidos. +branch.rename_protected_branch_failed=Este ramo está protegido por regras de salvaguarda baseadas em padrões glob. tag.create_tag=Criar etiqueta %s tag.create_tag_operation=Criar etiqueta @@ -3371,7 +3374,6 @@ monitor.execute_time=Tempo de execução monitor.last_execution_result=Resultado monitor.process.cancel=Cancelar processo monitor.process.cancel_desc=Cancelar um processo pode resultar na perda de dados -monitor.process.cancel_notices=Cancelar: <strong>%s</strong>? monitor.process.children=Descendentes monitor.queues=Filas @@ -3568,7 +3570,8 @@ conda.install=Para instalar o pacote usando o Conda, execute o seguinte comando: container.details.type=Tipo de imagem container.details.platform=Plataforma container.pull=Puxar a imagem usando a linha de comandos: -container.digest=Resumo: +container.images=Imagens +container.digest=Resumo container.multi_arch=S.O. / Arquit. container.layers=Camadas de imagem container.labels=Rótulos diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index 17ebb275a3..d3b673bd18 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -3176,7 +3176,6 @@ conda.install=Чтобы установить пакет с помощью Conda container.details.type=Тип образа container.details.platform=Платформа container.pull=Загрузите образ из командной строки: -container.digest=Отпечаток: container.multi_arch=ОС / архитектура container.layers=Слои образа container.labels=Метки diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index f64f4935a4..6d14f512ff 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -3430,7 +3430,6 @@ conda.install=Conda ile paket kurmak için aşağıdaki komutu çalıştırın: container.details.type=Görüntü Türü container.details.platform=Platform container.pull=Görüntüyü komut satırını kullanarak çekin: -container.digest=Özet: container.multi_arch=İşletim Sistemi / Mimari container.layers=Görüntü Katmanları container.labels=Etiketler diff --git a/options/locale/locale_zh-CN.ini b/options/locale/locale_zh-CN.ini index af590a68c1..92de8a1280 100644 --- a/options/locale/locale_zh-CN.ini +++ b/options/locale/locale_zh-CN.ini @@ -1678,16 +1678,13 @@ issues.timetracker_timer_manually_add=添加时间 issues.time_estimate_set=设置预计时间 issues.time_estimate_display=预计: %s -issues.change_time_estimate_at=将预计时间修改为 <b>%s</b> %s issues.remove_time_estimate_at=删除预计时间 %s issues.time_estimate_invalid=预计时间格式无效 issues.start_tracking_history=`开始工作 %s` issues.tracker_auto_close=当此工单关闭时,自动停止计时器 issues.tracking_already_started=`你已经开始对 <a href="%s">另一个工单</a> 进行时间跟踪!` -issues.stop_tracking_history=`停止工作 %s` issues.cancel_tracking_history=`取消时间跟踪 %s` issues.del_time=删除此时间跟踪日志 -issues.add_time_history=`添加计时 %s` issues.del_time_history=`已删除时间 %s` issues.add_time_manually=手动添加时间 issues.add_time_hours=小时 @@ -3359,7 +3356,6 @@ monitor.execute_time=执行时长 monitor.last_execution_result=结果 monitor.process.cancel=中止进程 monitor.process.cancel_desc=中止一个进程可能导致数据丢失 -monitor.process.cancel_notices=中止:<strong>%s</strong> ? monitor.process.children=子进程 monitor.queues=队列 @@ -3555,7 +3551,6 @@ conda.install=要使用 Conda 安装软件包,请运行以下命令: container.details.type=镜像类型 container.details.platform=平台 container.pull=从命令行拉取镜像: -container.digest=摘要: container.multi_arch=OS / Arch container.layers=镜像层 container.labels=标签 diff --git a/options/locale/locale_zh-TW.ini b/options/locale/locale_zh-TW.ini index b6e9e58fda..d03d9cf1fa 100644 --- a/options/locale/locale_zh-TW.ini +++ b/options/locale/locale_zh-TW.ini @@ -1672,16 +1672,13 @@ issues.timetracker_timer_manually_add=手動新增時間 issues.time_estimate_set=設定預估時間 issues.time_estimate_display=預估時間:%s -issues.change_time_estimate_at=將預估時間更改為 <b>%s</b> %s issues.remove_time_estimate_at=移除預估時間 %s issues.time_estimate_invalid=預估時間格式無效 issues.start_tracking_history=`開始工作 %s` issues.tracker_auto_close=當這個問題被關閉時,自動停止計時器 issues.tracking_already_started=`您已在<a href="%s">另一個問題</a>上開始時間追蹤!` -issues.stop_tracking_history=`結束工作 %s` issues.cancel_tracking_history=`取消時間追蹤 %s` issues.del_time=刪除此時間記錄 -issues.add_time_history=`加入了花費時間 %s` issues.del_time_history=`刪除了花費時間 %s` issues.add_time_manually=手動新增時間 issues.add_time_hours=小時 @@ -3350,7 +3347,6 @@ monitor.execute_time=已執行時間 monitor.last_execution_result=結果 monitor.process.cancel=結束處理程序 monitor.process.cancel_desc=結束處理程序可能造成資料遺失 -monitor.process.cancel_notices=結束: <strong>%s</strong>? monitor.process.children=子程序 monitor.queues=佇列 @@ -3546,7 +3542,6 @@ conda.install=執行下列命令以使用 Conda 安裝此套件: container.details.type=映像檔類型 container.details.platform=平台 container.pull=透過下列命令拉取映像檔: -container.digest=摘要: container.multi_arch=作業系統 / 架構 container.layers=映像檔 Layers container.labels=標籤 diff --git a/package-lock.json b/package-lock.json index 89bffd0ff3..b993e40e73 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,18 +6,18 @@ "": { "dependencies": { "@citation-js/core": "0.7.14", - "@citation-js/plugin-bibtex": "0.7.16", + "@citation-js/plugin-bibtex": "0.7.17", "@citation-js/plugin-csl": "0.7.14", "@citation-js/plugin-software-formats": "0.6.1", "@github/markdown-toolbar-element": "2.2.3", - "@github/relative-time-element": "4.4.4", + "@github/relative-time-element": "4.4.5", "@github/text-expander-element": "2.8.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.14.0", "@silverwind/vue3-calendar-heatmap": "2.0.6", "add-asset-webpack-plugin": "3.0.0", "ansi_up": "6.0.2", - "asciinema-player": "3.8.1", + "asciinema-player": "3.8.2", "chart.js": "4.4.7", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", @@ -29,11 +29,11 @@ "easymde": "2.18.0", "esbuild-loader": "4.2.2", "escape-goat": "4.0.0", - "fast-glob": "3.3.2", + "fast-glob": "3.3.3", "htmx.org": "2.0.4", - "idiomorph": "0.3.0", + "idiomorph": "0.4.0", "jquery": "3.7.1", - "katex": "0.16.18", + "katex": "0.16.20", "license-checker-webpack-plugin": "0.2.1", "mermaid": "11.4.1", "mini-css-extract-plugin": "2.9.2", @@ -42,7 +42,7 @@ "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", "perfect-debounce": "1.0.0", - "postcss": "8.4.49", + "postcss": "8.5.1", "postcss-loader": "8.1.1", "postcss-nesting": "13.0.1", "sortablejs": "1.15.6", @@ -53,7 +53,7 @@ "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", - "typescript": "5.7.2", + "typescript": "5.7.3", "uint8-to-base64": "0.2.0", "vanilla-colorful": "0.7.2", "vue": "3.5.13", @@ -61,14 +61,14 @@ "vue-chartjs": "5.3.2", "vue-loader": "17.4.2", "webpack": "5.97.1", - "webpack-cli": "5.1.4", + "webpack-cli": "6.0.1", "wrap-ansi": "9.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@playwright/test": "1.49.1", "@stoplight/spectral-cli": "6.14.2", - "@stylistic/eslint-plugin-js": "2.12.1", + "@stylistic/eslint-plugin-js": "2.13.0", "@stylistic/stylelint-plugin": "3.1.1", "@types/dropzone": "5.7.9", "@types/jquery": "3.5.32", @@ -80,8 +80,8 @@ "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/toastify-js": "1.12.3", - "@typescript-eslint/eslint-plugin": "8.18.1", - "@typescript-eslint/parser": "8.18.1", + "@typescript-eslint/eslint-plugin": "8.20.0", + "@typescript-eslint/parser": "8.20.0", "@vitejs/plugin-vue": "5.2.1", "eslint": "8.57.0", "eslint-import-resolver-typescript": "3.7.0", @@ -99,16 +99,16 @@ "eslint-plugin-vue": "9.32.0", "eslint-plugin-vue-scoped-css": "2.9.0", "eslint-plugin-wc": "2.2.0", - "happy-dom": "15.11.7", + "happy-dom": "16.6.0", "markdownlint-cli": "0.43.0", "nolyfill": "1.0.43", - "postcss-html": "1.7.0", - "stylelint": "16.12.0", + "postcss-html": "1.8.0", + "stylelint": "16.13.2", "stylelint-declaration-block-no-ignored-properties": "2.8.0", - "stylelint-declaration-strict-value": "1.10.6", + "stylelint-declaration-strict-value": "1.10.7", "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.3.2", - "type-fest": "4.30.2", + "type-fest": "4.32.0", "updates": "16.4.1", "vite-string-plugin": "1.3.4", "vitest": "2.1.8", @@ -191,9 +191,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", - "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", "dev": true, "license": "MIT", "engines": { @@ -281,14 +281,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -311,13 +311,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", - "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.25.9", + "@babel/compat-data": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -474,9 +474,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz", - "integrity": "sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, "license": "MIT", "engines": { @@ -502,15 +502,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/traverse": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -591,12 +591,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", - "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", + "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", "license": "MIT", "dependencies": { - "@babel/types": "^7.26.3" + "@babel/types": "^7.26.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -870,13 +870,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz", - "integrity": "sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -1098,14 +1098,14 @@ } }, "node_modules/@babel/plugin-transform-flow-strip-types": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.25.9.tgz", - "integrity": "sha512-/VVukELzPDdci7UUsWQaSkhgnjIWXnIyRpM02ldxaVoFK96c41So8JcKT3m0gYjyv7j5FNPGS5vfELrWalkbDA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.26.5.tgz", + "integrity": "sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-syntax-flow": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/plugin-syntax-flow": "^7.26.0" }, "engines": { "node": ">=6.9.0" @@ -1317,13 +1317,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz", - "integrity": "sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog==", + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.26.5" }, "engines": { "node": ">=6.9.0" @@ -1926,17 +1926,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.26.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", - "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz", + "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.3", - "@babel/parser": "^7.26.3", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.5", "@babel/template": "^7.25.9", - "@babel/types": "^7.26.3", + "@babel/types": "^7.26.5", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1955,9 +1955,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", - "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", + "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -1968,9 +1968,9 @@ } }, "node_modules/@braintree/sanitize-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.0.tgz", - "integrity": "sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", "license": "MIT" }, "node_modules/@chevrotain/cst-dts-gen": { @@ -2046,9 +2046,9 @@ } }, "node_modules/@citation-js/plugin-bibtex": { - "version": "0.7.16", - "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.16.tgz", - "integrity": "sha512-Udeli19VAoFjOw0H1bB1KgmekRoW6XP5cdR3OQF5c2Mt1tZatXWcSQVdq+FeLKzodRocZXG5NFRvjyUZjVbV6A==", + "version": "0.7.17", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.17.tgz", + "integrity": "sha512-pyMW6UR6iMPCk1mVwagNHabprajOCQO+TibxKI6ymdv5VOX3zoqeQF0utwjFnViquL/BZfM5SGUZCQdu+ZZYag==", "license": "MIT", "dependencies": { "@citation-js/date": "^0.5.0", @@ -2226,12 +2226,12 @@ } }, "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=14.17.0" } }, "node_modules/@dual-bundle/import-meta-resolve": { @@ -2808,9 +2808,9 @@ "license": "MIT" }, "node_modules/@github/relative-time-element": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.4.tgz", - "integrity": "sha512-Oi8uOL8O+ZWLD7dHRWCkm2cudcTYtB3VyOYf9BtzCgDGm+OKomyOREtItNMtWl1dxvec62BTKErq36uy+RYxQg==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.5.tgz", + "integrity": "sha512-9ejPtayBDIJfEU8x1fg/w2o5mahHkkp1SC6uObDtoKs4Gn+2a1vNK8XIiNDD8rMeEfpvDjydgSZZ+uk+7N0VsQ==", "license": "MIT" }, "node_modules/@github/text-expander-element": { @@ -3106,6 +3106,41 @@ "jsep": "^0.4.0||^1.0.0" } }, + "node_modules/@keyv/serialize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.2.tgz", + "integrity": "sha512-+E/LyaAeuABniD/RvUezWVXKpeuvwLEA9//nE9952zBaOdBd2mQ3pPoM8cUe2X6IcMByfuSLzmYqnYshG60+HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/@keyv/serialize/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", @@ -3364,9 +3399,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", - "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz", + "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==", "cpu": [ "arm" ], @@ -3378,9 +3413,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz", - "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz", + "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==", "cpu": [ "arm64" ], @@ -3392,9 +3427,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz", - "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz", + "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==", "cpu": [ "arm64" ], @@ -3406,9 +3441,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz", - "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz", + "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==", "cpu": [ "x64" ], @@ -3420,9 +3455,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz", - "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz", + "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==", "cpu": [ "arm64" ], @@ -3434,9 +3469,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz", - "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz", + "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==", "cpu": [ "x64" ], @@ -3448,9 +3483,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz", - "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz", + "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==", "cpu": [ "arm" ], @@ -3462,9 +3497,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz", - "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz", + "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==", "cpu": [ "arm" ], @@ -3476,9 +3511,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz", - "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz", + "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==", "cpu": [ "arm64" ], @@ -3490,9 +3525,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz", - "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz", + "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==", "cpu": [ "arm64" ], @@ -3504,9 +3539,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz", - "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz", + "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==", "cpu": [ "loong64" ], @@ -3518,9 +3553,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz", - "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz", + "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==", "cpu": [ "ppc64" ], @@ -3532,9 +3567,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz", - "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz", + "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==", "cpu": [ "riscv64" ], @@ -3546,9 +3581,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz", - "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz", + "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==", "cpu": [ "s390x" ], @@ -3560,9 +3595,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz", - "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz", + "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==", "cpu": [ "x64" ], @@ -3574,9 +3609,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz", - "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz", + "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==", "cpu": [ "x64" ], @@ -3588,9 +3623,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz", - "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz", + "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==", "cpu": [ "arm64" ], @@ -3602,9 +3637,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz", - "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz", + "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==", "cpu": [ "ia32" ], @@ -3616,9 +3651,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz", - "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz", + "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==", "cpu": [ "x64" ], @@ -4175,9 +4210,9 @@ } }, "node_modules/@stylistic/eslint-plugin-js": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.12.1.tgz", - "integrity": "sha512-5ybogtEgWIGCR6dMnaabztbWyVdAPDsf/5XOk6jBonWug875Q9/a6gm9QxnU3rhdyDEnckWKX7dduwYJMOWrVA==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.13.0.tgz", + "integrity": "sha512-GPPDK4+fcbsQD58a3abbng2Dx+jBoxM5cnYjBM4T24WFZRZdlNSKvR19TxP8CPevzMOodQ9QVzNeqWvMXzfJRA==", "dev": true, "license": "MIT", "dependencies": { @@ -4447,9 +4482,9 @@ "license": "MIT" }, "node_modules/@types/d3-shape": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", - "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -4611,9 +4646,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.10.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", - "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "version": "22.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", + "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -4767,21 +4802,21 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz", - "integrity": "sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz", + "integrity": "sha512-naduuphVw5StFfqp4Gq4WhIBE2gN1GEmMUExpJYknZJdRnc+2gDzB8Z3+5+/Kv33hPQRDGzQO/0opHE72lZZ6A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/type-utils": "8.18.1", - "@typescript-eslint/utils": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/type-utils": "8.20.0", + "@typescript-eslint/utils": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4797,16 +4832,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.1.tgz", - "integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.20.0.tgz", + "integrity": "sha512-gKXG7A5HMyjDIedBi6bUrDcun8GIjnI8qOwVLiY3rx6T/sHP/19XLJOnIq/FgQvWLHja5JN/LSE7eklNBr612g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/typescript-estree": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "debug": "^4.3.4" }, "engines": { @@ -4822,14 +4857,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz", - "integrity": "sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", + "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1" + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4840,16 +4875,16 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz", - "integrity": "sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.20.0.tgz", + "integrity": "sha512-bPC+j71GGvA7rVNAHAtOjbVXbLN5PkwqMvy1cwGeaxUoRQXVuKCebRoLzm+IPW/NtFFpstn1ummSIasD5t60GA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.18.1", - "@typescript-eslint/utils": "8.18.1", + "@typescript-eslint/typescript-estree": "8.20.0", + "@typescript-eslint/utils": "8.20.0", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4864,9 +4899,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz", - "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", + "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==", "dev": true, "license": "MIT", "engines": { @@ -4878,20 +4913,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz", - "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz", + "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/visitor-keys": "8.18.1", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4921,16 +4956,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.1.tgz", - "integrity": "sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", + "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.18.1", - "@typescript-eslint/types": "8.18.1", - "@typescript-eslint/typescript-estree": "8.18.1" + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4945,13 +4980,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz", - "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", + "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/types": "8.20.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -5080,6 +5115,13 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/snapshot": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.8.tgz", @@ -5105,6 +5147,13 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/spy": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.8.tgz", @@ -5470,42 +5519,42 @@ } }, "node_modules/@webpack-cli/configtest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", - "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-3.0.1.tgz", + "integrity": "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA==", "license": "MIT", "engines": { - "node": ">=14.15.0" + "node": ">=18.12.0" }, "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" } }, "node_modules/@webpack-cli/info": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", - "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-3.0.1.tgz", + "integrity": "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ==", "license": "MIT", "engines": { - "node": ">=14.15.0" + "node": ">=18.12.0" }, "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" } }, "node_modules/@webpack-cli/serve": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", - "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-3.0.1.tgz", + "integrity": "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg==", "license": "MIT", "engines": { - "node": ">=14.15.0" + "node": ">=18.12.0" }, "peerDependencies": { - "webpack": "5.x.x", - "webpack-cli": "5.x.x" + "webpack": "^5.82.0", + "webpack-cli": "6.x.x" }, "peerDependenciesMeta": { "webpack-dev-server": { @@ -5819,9 +5868,9 @@ } }, "node_modules/asciinema-player": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.8.1.tgz", - "integrity": "sha512-NkpbFg81Y6iJFpDRndakLCQ0G26XSpvuT3vJTFjMRgHb26lqHgRNY9gun54e5MehZ4fEDNYkMZv+z6MfZ8c2aA==", + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/asciinema-player/-/asciinema-player-3.8.2.tgz", + "integrity": "sha512-Lgcnj9u/H6sRpGRX1my7Azcay6llLmB/GVkCGcDbPwdTVTisS1ir8SQ9jRWRvjlLUjpSJkN0euruvy3sLRM8tw==", "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.21.0", @@ -6039,9 +6088,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "funding": [ { "type": "opencollective", @@ -6140,6 +6189,27 @@ "node": ">=8" } }, + "node_modules/cacheable": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.7.tgz", + "integrity": "sha512-AbfG7dAuYNjYxFUtL1lAqmlWdxczCJ47w7cFjhGcnGnUdwSo6VgmSojfoW3tUI12HUkgTJ5kqj78yyq6TsFtlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.6.0", + "keyv": "^5.2.3" + } + }, + "node_modules/cacheable/node_modules/keyv": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.2.3.tgz", + "integrity": "sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.0.2" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -6159,9 +6229,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", + "version": "1.0.30001692", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", + "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", "funding": [ { "type": "opencollective", @@ -6529,13 +6599,13 @@ "license": "MIT" }, "node_modules/core-js-compat": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", - "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.2" + "browserslist": "^4.24.3" }, "funding": { "type": "opencollective", @@ -6753,9 +6823,9 @@ "license": "MIT" }, "node_modules/cytoscape": { - "version": "3.30.4", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.4.tgz", - "integrity": "sha512-OxtlZwQl1WbwMmLiyPSEBuzeTIQnwZhJYYWFzZ2PhEHVFwpeaqNIkUzSiso00D98qk60l8Gwon2RP304d3BJ1A==", + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.31.0.tgz", + "integrity": "sha512-zDGn1K/tfZwEnoGOcHc0H4XazqAAXAuDpcYw9mUnUjATjqljyCNGJv8uEvbvxGaGHaVshxMecyl6oc6uKzRfbw==", "license": "MIT", "engines": { "node": ">=0.10" @@ -7461,9 +7531,9 @@ } }, "node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7505,9 +7575,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.74", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz", - "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==", + "version": "1.5.82", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.82.tgz", + "integrity": "sha512-Zq16uk1hfQhyGx5GpwPAYDwddJuSGhtRhgOA2mCxANYaDT79nAeGnaXogMGng4KqLaJUVnOnuL0+TDop9nLOiA==", "license": "ISC" }, "node_modules/emoji-regex": { @@ -7526,9 +7596,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -7595,9 +7665,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "license": "MIT" }, "node_modules/esbuild": { @@ -7764,13 +7834,13 @@ } }, "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", + "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", "dev": true, "license": "MIT", "bin": { - "eslint-config-prettier": "bin/cli.js" + "eslint-config-prettier": "build/bin/cli.js" }, "peerDependencies": { "eslint": ">=7.0.0" @@ -8221,9 +8291,9 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.2.tgz", + "integrity": "sha512-1yI3/hf35wmlq66C8yOyrujQnel+v5l1Vop5Cl2I6ylyNTT1JbuUUnV3/41PzwTzcyDp/oF0jWE3HXvcH5AQOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8526,6 +8596,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/eslint-plugin-vitest/node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/eslint-plugin-vue": { "version": "9.32.0", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.32.0.tgz", @@ -8820,16 +8903,16 @@ "license": "Apache-2.0" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -8868,9 +8951,19 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", + "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], "license": "BSD-3-Clause" }, "node_modules/fastest-levenshtein": { @@ -8883,9 +8976,9 @@ } }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -9311,13 +9404,12 @@ } }, "node_modules/happy-dom": { - "version": "15.11.7", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.11.7.tgz", - "integrity": "sha512-KyrFvnl+J9US63TEzwoiJOQzZBJY7KgBushJA8X61DMbNsH+2ONkDuLDnCnwUiPTF42tLoEmrPyoqbenVA5zrg==", + "version": "16.6.0", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-16.6.0.tgz", + "integrity": "sha512-Zz5S9sog8a3p8XYZbO+eI1QMOAvCNnIoyrH8A8MLX+X2mJrzADTy+kdETmc4q+uD9AGAvQYGn96qBAn2RAciKw==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^4.5.0", "webidl-conversions": "^7.0.0", "whatwg-mimetype": "^3.0.0" }, @@ -9361,6 +9453,13 @@ "he": "bin/he" } }, + "node_modules/hookified": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.6.0.tgz", + "integrity": "sha512-se7cpwTA+iA/eY548Bu03JJqBiEZAqU2jnyKdj5B5qurtBg64CZGHTgqCv4Yh7NWu6FGI09W61MCq+NoPj9GXA==", + "dev": true, + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -9442,10 +9541,10 @@ } }, "node_modules/idiomorph": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.3.0.tgz", - "integrity": "sha512-UhV1Ey5xCxIwR9B+OgIjQa+1Jx99XQ1vQHUsKBU1RpQzCx1u+b+N6SOXgf5mEJDqemUI/ffccu6+71l2mJUsRA==", - "license": "BSD 2-Clause" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/idiomorph/-/idiomorph-0.4.0.tgz", + "integrity": "sha512-VdXFpZOTXhLatJmhCWJR5oQKLXT01O6sFCJqT0/EqG71C4tYZdPJ5etvttwWsT2WKRYWz160XkNr1DUqXNMZHg==", + "license": "BSD-2-Clause" }, "node_modules/ieee754": { "version": "1.2.1", @@ -10024,9 +10123,9 @@ "license": "MIT" }, "node_modules/katex": { - "version": "0.16.18", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.18.tgz", - "integrity": "sha512-LRuk0rPdXrecAFwQucYjMiIs0JFefk6N1q/04mlw14aVIVgxq1FO0MA9RiIIGVaKOB5GIP5GH4aBBNraZERmaQ==", + "version": "0.16.20", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.20.tgz", + "integrity": "sha512-jjuLaMGD/7P8jUTpdKhA9IoqnH+yMFB3sdAFtq5QdAqeP2PjiSbnC3EaguKPNtv6dXXanHxp1ckwvF4a86LBig==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -10506,9 +10605,9 @@ } }, "node_modules/markdownlint-cli/node_modules/glob": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", - "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", "dev": true, "license": "ISC", "dependencies": { @@ -10763,14 +10862,14 @@ } }, "node_modules/mlly": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.3.tgz", - "integrity": "sha512-xUsx5n/mN0uQf4V548PKQ+YShA4/IW0KI1dZhrNrPCLG+xizETbHTkOa1f8/xut9JRPp8kQuMnz0oqwkTiLo/A==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", "license": "MIT", "dependencies": { "acorn": "^8.14.0", - "pathe": "^1.1.2", - "pkg-types": "^1.2.1", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, @@ -11138,9 +11237,9 @@ "license": "BlueOak-1.0.0" }, "node_modules/package-manager-detector": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.7.tgz", - "integrity": "sha512-g4+387DXDKlZzHkP+9FLt8yKj8+/3tOkPv7DVTJGGRm00RkEWgqbFstX1mXJ4M0VDYhUqsTOiISqNOJnhAu3PQ==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.8.tgz", + "integrity": "sha512-ts9KSdroZisdvKMWVAVCXiKqnqNfXz4+IbrBG8/BWx/TR5le+jfenvoBuIZ6UWM9nz47W7AbD9qYfAwfWMIwzA==", "license": "MIT" }, "node_modules/parent-module": { @@ -11257,9 +11356,9 @@ } }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.1.tgz", + "integrity": "sha512-6jpjMpOth5S9ITVu5clZ7NOgHNsv5vRQdheL9ztp2vZmM6fRbLvyua1tiBIL4lk8SAe3ARzeXEly6siXCjDHDw==", "license": "MIT" }, "node_modules/pathval": { @@ -11385,14 +11484,14 @@ } }, "node_modules/pkg-types": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz", - "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", "license": "MIT", "dependencies": { "confbox": "^0.1.8", - "mlly": "^1.7.2", - "pathe": "^1.1.2" + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, "node_modules/playwright": { @@ -11464,9 +11563,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "funding": [ { "type": "opencollective", @@ -11483,7 +11582,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -11492,15 +11591,15 @@ } }, "node_modules/postcss-html": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.7.0.tgz", - "integrity": "sha512-MfcMpSUIaR/nNgeVS8AyvyDugXlADjN9AcV7e5rDfrF1wduIAGSkL4q2+wgrZgA3sHVAHLDO9FuauHhZYW2nBw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.8.0.tgz", + "integrity": "sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==", "dev": true, "license": "MIT", "dependencies": { "htmlparser2": "^8.0.0", "js-tokens": "^9.0.0", - "postcss": "^8.4.0", + "postcss": "^8.5.0", "postcss-safe-parser": "^6.0.0" }, "engines": { @@ -12322,9 +12421,9 @@ } }, "node_modules/resolve": { - "version": "1.22.9", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz", - "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -12334,6 +12433,9 @@ "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12584,18 +12686,18 @@ } }, "node_modules/seroval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.1.1.tgz", - "integrity": "sha512-rqEO6FZk8mv7Hyv4UCj3FD3b6Waqft605TLfsCe/BiaylRpyyMC0b+uA5TJKawX3KzMrdi3wsLbCaLplrQmBvQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.2.0.tgz", + "integrity": "sha512-GURoU99ko2UiAgUC3qDCk59Jb3Ss4Po8VIMGkG8j5PFo2Q7y0YSMP8QG9NuL/fJCoTz9V1XZUbpNIMXPOfaGpA==", "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/seroval-plugins": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.1.1.tgz", - "integrity": "sha512-qNSy1+nUj7hsCOon7AO4wdAIo9P0jrzAMp18XhiOzA6/uO5TKtP7ScozVJ8T293oRIvi5wyCHSM4TrJo/c/GJA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.2.0.tgz", + "integrity": "sha512-hULTbfzSe81jGWLH8TAJjkEvw6JWMqOo9Uq+4V4vg+HNq53hyHldM9ZOfjdzokcFysiTp9aFdV2vJpZFqKeDjQ==", "license": "MIT", "engines": { "node": ">=10" @@ -12711,9 +12813,9 @@ } }, "node_modules/solid-js": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.3.tgz", - "integrity": "sha512-5ba3taPoZGt9GY3YlsCB24kCg0Lv/rie/HTD4kG6h4daZZz7+yK02xn8Vx8dLYBc9i6Ps5JwAbEiqjmKaLB3Ag==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.4.tgz", + "integrity": "sha512-ipQl8FJ31bFUoBNScDQTG3BjN6+9Rg+Q+f10bUbnO6EOTTf5NGerJeHc7wyu5I4RMHEl/WwZwUmy/PTRgxxZ8g==", "license": "MIT", "dependencies": { "csstype": "^3.1.0", @@ -12829,9 +12931,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.20", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", - "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", "license": "CC0-1.0" }, "node_modules/spdx-ranges": { @@ -13021,9 +13123,9 @@ "license": "ISC" }, "node_modules/stylelint": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.12.0.tgz", - "integrity": "sha512-F8zZ3L/rBpuoBZRvI4JVT20ZanPLXfQLzMOZg1tzPflRVh9mKpOZ8qcSIhh1my3FjAjZWG4T2POwGnmn6a6hbg==", + "version": "16.13.2", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.13.2.tgz", + "integrity": "sha512-wDlgh0mRO9RtSa3TdidqHd0nOG8MmUyVKl+dxA6C1j8aZRzpNeEgdhFmU5y4sZx4Fc6r46p0fI7p1vR5O2DZqA==", "dev": true, "funding": [ { @@ -13046,16 +13148,16 @@ "colord": "^2.9.3", "cosmiconfig": "^9.0.0", "css-functions-list": "^3.2.3", - "css-tree": "^3.0.1", + "css-tree": "^3.1.0", "debug": "^4.3.7", - "fast-glob": "^3.3.2", + "fast-glob": "^3.3.3", "fastest-levenshtein": "^1.0.16", - "file-entry-cache": "^9.1.0", + "file-entry-cache": "^10.0.5", "global-modules": "^2.0.0", "globby": "^11.1.0", "globjoin": "^0.1.4", "html-tags": "^3.3.1", - "ignore": "^6.0.2", + "ignore": "^7.0.1", "imurmurhash": "^0.1.4", "is-plain-object": "^5.0.0", "known-css-properties": "^0.35.0", @@ -13097,9 +13199,9 @@ } }, "node_modules/stylelint-declaration-strict-value": { - "version": "1.10.6", - "resolved": "https://registry.npmjs.org/stylelint-declaration-strict-value/-/stylelint-declaration-strict-value-1.10.6.tgz", - "integrity": "sha512-aZGEW4Ee26Tx4UvpQJbcElVXZ42EleujEByiyKDTT7t83EeSe9t0lAG3OOLJnnvLjz/dQnp+L+3IYTMeQI51vQ==", + "version": "1.10.7", + "resolved": "https://registry.npmjs.org/stylelint-declaration-strict-value/-/stylelint-declaration-strict-value-1.10.7.tgz", + "integrity": "sha512-FlMvc3uoQtMcItW3Zh8lHJ7oN2KGns3vZDCaTZoGFRiRIjImQoxO+6gAeRf+Dgi0nXFICIPq9xxFsMi8zuYUsg==", "dev": true, "license": "MIT", "engines": { @@ -13181,36 +13283,31 @@ "license": "MIT" }, "node_modules/stylelint/node_modules/file-entry-cache": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz", - "integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==", + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.5.tgz", + "integrity": "sha512-umpQsJrBNsdMDgreSryMEXvJh66XeLtZUwA8Gj7rHGearGufUFv6rB/bcXRFsiGWw/VeSUgUofF4Rf2UKEOrTA==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^5.0.0" - }, - "engines": { - "node": ">=18" + "flat-cache": "^6.1.5" } }, "node_modules/stylelint/node_modules/flat-cache": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz", - "integrity": "sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.5.tgz", + "integrity": "sha512-QR+2kN38f8nMfiIQ1LHYjuDEmZNZVjxuxY+HufbS3BW0EX01Q5OnH7iduOYRutmgiXb797HAKcXUeXrvRjjgSQ==", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.3.1", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=18" + "cacheable": "^1.8.7", + "flatted": "^3.3.2", + "hookified": "^1.6.0" } }, "node_modules/stylelint/node_modules/ignore": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", - "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz", + "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==", "dev": true, "license": "MIT", "engines": { @@ -13269,9 +13366,9 @@ } }, "node_modules/stylis": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz", - "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.5.tgz", + "integrity": "sha512-K7npNOKGRYuhAFFzkzMGfxFDpN6gDwf8hcMiE+uveTVbBgm93HrNP3ZDUpKqzZ4pG7TP6fmb+EMAQPjq9FqqvA==", "license": "MIT" }, "node_modules/stylus": { @@ -13740,9 +13837,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", - "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "license": "MIT" }, "node_modules/tinypool": { @@ -13815,16 +13912,16 @@ "license": "MIT" }, "node_modules/ts-api-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", - "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-dedent": { @@ -13889,9 +13986,9 @@ } }, "node_modules/type-fest": { - "version": "4.30.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.30.2.tgz", - "integrity": "sha512-UJShLPYi1aWqCdq9HycOL/gwsuqda1OISdBO3t8RlXQC4QvtuIz4b5FCfe2dQIWEpmlRExKmcTBfP1r9bhY7ig==", + "version": "4.32.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.32.0.tgz", + "integrity": "sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -13902,9 +13999,9 @@ } }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -14000,9 +14097,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "funding": [ { "type": "opencollective", @@ -14020,7 +14117,7 @@ "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -14197,6 +14294,13 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vite-string-plugin": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/vite-string-plugin/-/vite-string-plugin-1.3.4.tgz", @@ -14227,9 +14331,9 @@ } }, "node_modules/vite/node_modules/rollup": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz", - "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==", + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz", + "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==", "dev": true, "license": "MIT", "dependencies": { @@ -14243,25 +14347,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.28.1", - "@rollup/rollup-android-arm64": "4.28.1", - "@rollup/rollup-darwin-arm64": "4.28.1", - "@rollup/rollup-darwin-x64": "4.28.1", - "@rollup/rollup-freebsd-arm64": "4.28.1", - "@rollup/rollup-freebsd-x64": "4.28.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.28.1", - "@rollup/rollup-linux-arm-musleabihf": "4.28.1", - "@rollup/rollup-linux-arm64-gnu": "4.28.1", - "@rollup/rollup-linux-arm64-musl": "4.28.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.28.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1", - "@rollup/rollup-linux-riscv64-gnu": "4.28.1", - "@rollup/rollup-linux-s390x-gnu": "4.28.1", - "@rollup/rollup-linux-x64-gnu": "4.28.1", - "@rollup/rollup-linux-x64-musl": "4.28.1", - "@rollup/rollup-win32-arm64-msvc": "4.28.1", - "@rollup/rollup-win32-ia32-msvc": "4.28.1", - "@rollup/rollup-win32-x64-msvc": "4.28.1", + "@rollup/rollup-android-arm-eabi": "4.30.1", + "@rollup/rollup-android-arm64": "4.30.1", + "@rollup/rollup-darwin-arm64": "4.30.1", + "@rollup/rollup-darwin-x64": "4.30.1", + "@rollup/rollup-freebsd-arm64": "4.30.1", + "@rollup/rollup-freebsd-x64": "4.30.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.30.1", + "@rollup/rollup-linux-arm-musleabihf": "4.30.1", + "@rollup/rollup-linux-arm64-gnu": "4.30.1", + "@rollup/rollup-linux-arm64-musl": "4.30.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.30.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1", + "@rollup/rollup-linux-riscv64-gnu": "4.30.1", + "@rollup/rollup-linux-s390x-gnu": "4.30.1", + "@rollup/rollup-linux-x64-gnu": "4.30.1", + "@rollup/rollup-linux-x64-musl": "4.30.1", + "@rollup/rollup-win32-arm64-msvc": "4.30.1", + "@rollup/rollup-win32-ia32-msvc": "4.30.1", + "@rollup/rollup-win32-x64-msvc": "4.30.1", "fsevents": "~2.3.2" } }, @@ -14341,6 +14445,13 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", @@ -14595,42 +14706,39 @@ } }, "node_modules/webpack-cli": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", - "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-6.0.1.tgz", + "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "license": "MIT", "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^2.1.1", - "@webpack-cli/info": "^2.0.2", - "@webpack-cli/serve": "^2.0.5", + "@discoveryjs/json-ext": "^0.6.1", + "@webpack-cli/configtest": "^3.0.1", + "@webpack-cli/info": "^3.0.1", + "@webpack-cli/serve": "^3.0.1", "colorette": "^2.0.14", - "commander": "^10.0.1", + "commander": "^12.1.0", "cross-spawn": "^7.0.3", - "envinfo": "^7.7.3", + "envinfo": "^7.14.0", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^3.1.1", "rechoir": "^0.8.0", - "webpack-merge": "^5.7.3" + "webpack-merge": "^6.0.1" }, "bin": { "webpack-cli": "bin/cli.js" }, "engines": { - "node": ">=14.15.0" + "node": ">=18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "5.x.x" + "webpack": "^5.82.0" }, "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, "webpack-bundle-analyzer": { "optional": true }, @@ -14640,26 +14748,26 @@ } }, "node_modules/webpack-cli/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "license": "MIT", "engines": { - "node": ">=14" + "node": ">=18" } }, "node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", - "wildcard": "^2.0.0" + "wildcard": "^2.0.1" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" } }, "node_modules/webpack-sources": { @@ -14977,9 +15085,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 31a65c647c..2cda2ae844 100644 --- a/package.json +++ b/package.json @@ -5,18 +5,18 @@ }, "dependencies": { "@citation-js/core": "0.7.14", - "@citation-js/plugin-bibtex": "0.7.16", + "@citation-js/plugin-bibtex": "0.7.17", "@citation-js/plugin-csl": "0.7.14", "@citation-js/plugin-software-formats": "0.6.1", "@github/markdown-toolbar-element": "2.2.3", - "@github/relative-time-element": "4.4.4", + "@github/relative-time-element": "4.4.5", "@github/text-expander-element": "2.8.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.14.0", "@silverwind/vue3-calendar-heatmap": "2.0.6", "add-asset-webpack-plugin": "3.0.0", "ansi_up": "6.0.2", - "asciinema-player": "3.8.1", + "asciinema-player": "3.8.2", "chart.js": "4.4.7", "chartjs-adapter-dayjs-4": "1.0.4", "chartjs-plugin-zoom": "2.2.0", @@ -28,11 +28,11 @@ "easymde": "2.18.0", "esbuild-loader": "4.2.2", "escape-goat": "4.0.0", - "fast-glob": "3.3.2", + "fast-glob": "3.3.3", "htmx.org": "2.0.4", - "idiomorph": "0.3.0", + "idiomorph": "0.4.0", "jquery": "3.7.1", - "katex": "0.16.18", + "katex": "0.16.20", "license-checker-webpack-plugin": "0.2.1", "mermaid": "11.4.1", "mini-css-extract-plugin": "2.9.2", @@ -41,7 +41,7 @@ "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", "perfect-debounce": "1.0.0", - "postcss": "8.4.49", + "postcss": "8.5.1", "postcss-loader": "8.1.1", "postcss-nesting": "13.0.1", "sortablejs": "1.15.6", @@ -52,7 +52,7 @@ "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", - "typescript": "5.7.2", + "typescript": "5.7.3", "uint8-to-base64": "0.2.0", "vanilla-colorful": "0.7.2", "vue": "3.5.13", @@ -60,14 +60,14 @@ "vue-chartjs": "5.3.2", "vue-loader": "17.4.2", "webpack": "5.97.1", - "webpack-cli": "5.1.4", + "webpack-cli": "6.0.1", "wrap-ansi": "9.0.0" }, "devDependencies": { "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@playwright/test": "1.49.1", "@stoplight/spectral-cli": "6.14.2", - "@stylistic/eslint-plugin-js": "2.12.1", + "@stylistic/eslint-plugin-js": "2.13.0", "@stylistic/stylelint-plugin": "3.1.1", "@types/dropzone": "5.7.9", "@types/jquery": "3.5.32", @@ -79,8 +79,8 @@ "@types/throttle-debounce": "5.0.2", "@types/tinycolor2": "1.4.6", "@types/toastify-js": "1.12.3", - "@typescript-eslint/eslint-plugin": "8.18.1", - "@typescript-eslint/parser": "8.18.1", + "@typescript-eslint/eslint-plugin": "8.20.0", + "@typescript-eslint/parser": "8.20.0", "@vitejs/plugin-vue": "5.2.1", "eslint": "8.57.0", "eslint-import-resolver-typescript": "3.7.0", @@ -98,16 +98,16 @@ "eslint-plugin-vue": "9.32.0", "eslint-plugin-vue-scoped-css": "2.9.0", "eslint-plugin-wc": "2.2.0", - "happy-dom": "15.11.7", + "happy-dom": "16.6.0", "markdownlint-cli": "0.43.0", "nolyfill": "1.0.43", - "postcss-html": "1.7.0", - "stylelint": "16.12.0", + "postcss-html": "1.8.0", + "stylelint": "16.13.2", "stylelint-declaration-block-no-ignored-properties": "2.8.0", - "stylelint-declaration-strict-value": "1.10.6", + "stylelint-declaration-strict-value": "1.10.7", "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.3.2", - "type-fest": "4.30.2", + "type-fest": "4.32.0", "updates": "16.4.1", "vite-string-plugin": "1.3.4", "vitest": "2.1.8", diff --git a/poetry.lock b/poetry.lock index 5b9029eb8c..8c01674966 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,14 +1,14 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -42,33 +42,33 @@ six = ">=1.13.0" [[package]] name = "djlint" -version = "1.36.3" +version = "1.36.4" description = "HTML Template Linter and Formatter" optional = false python-versions = ">=3.9" files = [ - {file = "djlint-1.36.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ae7c620b58e16d6bf003bd7de3f71376a7a3daa79dc02e77f3726d5a75243f2"}, - {file = "djlint-1.36.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e155ce0970d4a28d0a2e9f2e106733a2ad05910eee90e056b056d48049e4a97b"}, - {file = "djlint-1.36.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e8bb0406e60cc696806aa6226df137618f3889c72f2dbdfa76c908c99151579"}, - {file = "djlint-1.36.3-cp310-cp310-win_amd64.whl", hash = "sha256:76d32faf988ad58ef2e7a11d04046fc984b98391761bf1b61f9a6044da53d414"}, - {file = "djlint-1.36.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:32f7a5834000fff22e94d1d35f95aaf2e06f2af2cae18af0ed2a4e215d60e730"}, - {file = "djlint-1.36.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3eb1b9c0be499e63e8822a051e7e55f188ff1ab8172a85d338a8ae21c872060e"}, - {file = "djlint-1.36.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c2e0dd1f26eb472b8c84eb70d6482877b6497a1fd031d7534864088f016d5ea"}, - {file = "djlint-1.36.3-cp311-cp311-win_amd64.whl", hash = "sha256:a06b531ab9d049c46ad4d2365d1857004a1a9dd0c23c8eae94aa0d233c6ec00d"}, - {file = "djlint-1.36.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e66361a865e5e5a4bbcb40f56af7f256fd02cbf9d48b763a40172749cc294084"}, - {file = "djlint-1.36.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:36e102b80d83e9ac2e6be9a9ded32fb925945f6dbc7a7156e4415de1b0aa0dba"}, - {file = "djlint-1.36.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ac4b7370d80bd82281e57a470de8923ac494ffb571b89d8787cef57c738c69a"}, - {file = "djlint-1.36.3-cp312-cp312-win_amd64.whl", hash = "sha256:107cc56bbef13d60cc0ae774a4d52881bf98e37c02412e573827a3e549217e3a"}, - {file = "djlint-1.36.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2a9f51971d6e63c41ea9b3831c928e1f21ae6fe57e87a3452cfe672d10232433"}, - {file = "djlint-1.36.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:080c98714b55d8f0fef5c42beaee8247ebb2e3d46b0936473bd6c47808bb6302"}, - {file = "djlint-1.36.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f65a80e0b5cb13d357ea51ca6570b34c2d9d18974c1e57142de760ea27d49ed0"}, - {file = "djlint-1.36.3-cp313-cp313-win_amd64.whl", hash = "sha256:95ef6b67ef7f2b90d9434bba37d572031079001dc8524add85c00ef0386bda1e"}, - {file = "djlint-1.36.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e2317a32094d525bc41cd11c8dc064bf38d1b442c99cc3f7c4a2616b5e6ce6e"}, - {file = "djlint-1.36.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e82266c28793cd15f97b93535d72bfbc77306eaaf6b210dd90910383a814ee6c"}, - {file = "djlint-1.36.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01b2101c2d1b079e8d545e6d9d03487fcca14d2371e44cbfdedee15b0bf4567c"}, - {file = "djlint-1.36.3-cp39-cp39-win_amd64.whl", hash = "sha256:15cde63ef28beb5194ff4137883025f125676ece1b574b64a3e1c6daed734639"}, - {file = "djlint-1.36.3-py3-none-any.whl", hash = "sha256:0c05cd5b76785de2c41a2420c06ffd112800bfc0f9c0f399cc7cea7c42557f4c"}, - {file = "djlint-1.36.3.tar.gz", hash = "sha256:d85735da34bc7ac93ad8ef9b4822cc2a23d5f0ce33f25438737b8dca1d404f78"}, + {file = "djlint-1.36.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2dfb60883ceb92465201bfd392291a7597c6752baede6fbb6f1980cac8d6c5c"}, + {file = "djlint-1.36.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc6a1320c0030244b530ac200642f883d3daa451a115920ef3d56d08b644292"}, + {file = "djlint-1.36.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3164a048c7bb0baf042387b1e33f9bbbf99d90d1337bb4c3d66eb0f96f5400a1"}, + {file = "djlint-1.36.4-cp310-cp310-win_amd64.whl", hash = "sha256:3196d5277da5934962d67ad6c33a948ba77a7b6eadf064648bef6ee5f216b03c"}, + {file = "djlint-1.36.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d68da0ed10ee9ca1e32e225cbb8e9b98bf7e6f8b48a8e4836117b6605b88cc7"}, + {file = "djlint-1.36.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c0478d5392247f1e6ee29220bbdbf7fb4e1bc0e7e83d291fda6fb926c1787ba7"}, + {file = "djlint-1.36.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:962f7b83aee166e499eff916d631c6dde7f1447d7610785a60ed2a75a5763483"}, + {file = "djlint-1.36.4-cp311-cp311-win_amd64.whl", hash = "sha256:53cbc450aa425c832f09bc453b8a94a039d147b096740df54a3547fada77ed08"}, + {file = "djlint-1.36.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff9faffd7d43ac20467493fa71d5355b5b330a00ade1c4d1e859022f4195223b"}, + {file = "djlint-1.36.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:79489e262b5ac23a8dfb7ca37f1eea979674cfc2d2644f7061d95bea12c38f7e"}, + {file = "djlint-1.36.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e58c5fa8c6477144a0be0a87273706a059e6dd0d6efae01146ae8c29cdfca675"}, + {file = "djlint-1.36.4-cp312-cp312-win_amd64.whl", hash = "sha256:bb6903777bf3124f5efedcddf1f4716aef097a7ec4223fc0fa54b865829a6e08"}, + {file = "djlint-1.36.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ead475013bcac46095b1bbc8cf97ed2f06e83422335734363f8a76b4ba7e47c2"}, + {file = "djlint-1.36.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6c601dfa68ea253311deb4a29a7362b7a64933bdfcfb5a06618f3e70ad1fa835"}, + {file = "djlint-1.36.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda5014f295002363381969864addeb2db13955f1b26e772657c3b273ed7809f"}, + {file = "djlint-1.36.4-cp313-cp313-win_amd64.whl", hash = "sha256:16ce37e085afe5a30953b2bd87cbe34c37843d94c701fc68a2dda06c1e428ff4"}, + {file = "djlint-1.36.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:89678661888c03d7bc6cadd75af69db29962b5ecbf93a81518262f5c48329f04"}, + {file = "djlint-1.36.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5b01a98df3e1ab89a552793590875bc6e954cad661a9304057db75363d519fa0"}, + {file = "djlint-1.36.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dabbb4f7b93223d471d09ae34ed515fef98b2233cbca2449ad117416c44b1351"}, + {file = "djlint-1.36.4-cp39-cp39-win_amd64.whl", hash = "sha256:7a483390d17e44df5bc23dcea29bdf6b63f3ed8b4731d844773a4829af4f5e0b"}, + {file = "djlint-1.36.4-py3-none-any.whl", hash = "sha256:e9699b8ac3057a6ed04fb90835b89bee954ed1959c01541ce4f8f729c938afdd"}, + {file = "djlint-1.36.4.tar.gz", hash = "sha256:17254f218b46fe5a714b224c85074c099bcb74e3b2e1f15c2ddc2cf415a408a1"}, ] [package.dependencies] @@ -82,15 +82,17 @@ pyyaml = ">=6" regex = ">=2023" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} tqdm = ">=4.62.2" +typing-extensions = {version = ">=3.6.6", markers = "python_version < \"3.11\""} [[package]] name = "editorconfig" -version = "0.12.4" +version = "0.17.0" description = "EditorConfig File Locator and Interpreter for Python" optional = false python-versions = "*" files = [ - {file = "EditorConfig-0.12.4.tar.gz", hash = "sha256:24857fa1793917dd9ccf0c7810a07e05404ce9b823521c7dce22a4fb5d125f80"}, + {file = "EditorConfig-0.17.0-py3-none-any.whl", hash = "sha256:fe491719c5f65959ec00b167d07740e7ffec9a3f362038c72b289330b9991dfc"}, + {file = "editorconfig-0.17.0.tar.gz", hash = "sha256:8739052279699840065d3a9f5c125d7d5a98daeefe53b0e5274261d77cb49aa2"}, ] [[package]] @@ -370,6 +372,17 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + [[package]] name = "yamllint" version = "1.35.1" @@ -391,4 +404,4 @@ dev = ["doc8", "flake8", "flake8-import-order", "rstcheck[sphinx]", "sphinx"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "01b1e2f910276dd20a70ebb665c83415c37531709d90874f5b7a86a5305e2369" +content-hash = "f2e8260efe6e25f77ef387daff9551e41d25027e4794b42bc7a851ed0dfafd85" diff --git a/pyproject.toml b/pyproject.toml index 4d36b30ea7..851504e72b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ package-mode = false python = "^3.10" [tool.poetry.group.dev.dependencies] -djlint = "1.36.3" +djlint = "1.36.4" yamllint = "1.35.1" [tool.djlint] diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 592879235c..a5ea752cf1 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -12,6 +12,7 @@ import ( "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" "code.gitea.io/gitea/models/organization" + repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/gitrepo" @@ -443,7 +444,14 @@ func UpdateBranch(ctx *context.APIContext) { msg, err := repo_service.RenameBranch(ctx, repo, ctx.Doer, ctx.Repo.GitRepo, oldName, opt.Name) if err != nil { - ctx.Error(http.StatusInternalServerError, "RenameBranch", err) + switch { + case repo_model.IsErrUserDoesNotHaveAccessToRepo(err): + ctx.Error(http.StatusForbidden, "", "User must be a repo or site admin to rename default or protected branches.") + case errors.Is(err, git_model.ErrBranchIsProtected): + ctx.Error(http.StatusForbidden, "", "Branch is protected by glob-based protection rules.") + default: + ctx.Error(http.StatusInternalServerError, "RenameBranch", err) + } return } if msg == "target_exist" { diff --git a/routers/api/v1/repo/hook_test.go b/routers/api/v1/repo/hook_test.go index c659a16f54..2d15c6e078 100644 --- a/routers/api/v1/repo/hook_test.go +++ b/routers/api/v1/repo/hook_test.go @@ -23,7 +23,7 @@ func TestTestHook(t *testing.T) { contexttest.LoadRepoCommit(t, ctx) contexttest.LoadUser(t, ctx, 2) TestHook(ctx) - assert.EqualValues(t, http.StatusNoContent, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusNoContent, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{ HookID: 1, diff --git a/routers/api/v1/repo/repo_test.go b/routers/api/v1/repo/repo_test.go index 8d6ca9e3b5..0a63b16a99 100644 --- a/routers/api/v1/repo/repo_test.go +++ b/routers/api/v1/repo/repo_test.go @@ -58,7 +58,7 @@ func TestRepoEdit(t *testing.T) { web.SetForm(ctx, &opts) Edit(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ ID: 1, }, unittest.Cond("name = ? AND is_archived = 1", *opts.Name)) @@ -78,7 +78,7 @@ func TestRepoEditNameChange(t *testing.T) { web.SetForm(ctx, &opts) Edit(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ ID: 1, diff --git a/routers/common/middleware.go b/routers/common/middleware.go index 12b0c67b01..2ba02de8ed 100644 --- a/routers/common/middleware.go +++ b/routers/common/middleware.go @@ -9,6 +9,7 @@ import ( "strings" "code.gitea.io/gitea/modules/cache" + "code.gitea.io/gitea/modules/gtprof" "code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/setting" @@ -43,14 +44,26 @@ func ProtocolMiddlewares() (handlers []any) { func RequestContextHandler() func(h http.Handler) http.Handler { return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - profDesc := fmt.Sprintf("%s: %s", req.Method, req.RequestURI) + return http.HandlerFunc(func(respOrig http.ResponseWriter, req *http.Request) { + // this response writer might not be the same as the one in context.Base.Resp + // because there might be a "gzip writer" in the middle, so the "written size" here is the compressed size + respWriter := context.WrapResponseWriter(respOrig) + + profDesc := fmt.Sprintf("HTTP: %s %s", req.Method, req.RequestURI) ctx, finished := reqctx.NewRequestContext(req.Context(), profDesc) defer finished() + ctx, span := gtprof.GetTracer().Start(ctx, gtprof.TraceSpanHTTP) + req = req.WithContext(ctx) + defer func() { + chiCtx := chi.RouteContext(req.Context()) + span.SetAttributeString(gtprof.TraceAttrHTTPRoute, chiCtx.RoutePattern()) + span.End() + }() + defer func() { if err := recover(); err != nil { - RenderPanicErrorPage(resp, req, err) // it should never panic + RenderPanicErrorPage(respWriter, req, err) // it should never panic } }() @@ -62,7 +75,7 @@ func RequestContextHandler() func(h http.Handler) http.Handler { _ = req.MultipartForm.RemoveAll() // remove the temp files buffered to tmp directory } }) - next.ServeHTTP(context.WrapResponseWriter(resp), req) + next.ServeHTTP(respWriter, req) }) } } @@ -71,11 +84,11 @@ func ChiRoutePathHandler() func(h http.Handler) http.Handler { // make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly return func(next http.Handler) http.Handler { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - ctx := chi.RouteContext(req.Context()) + chiCtx := chi.RouteContext(req.Context()) if req.URL.RawPath == "" { - ctx.RoutePath = req.URL.EscapedPath() + chiCtx.RoutePath = req.URL.EscapedPath() } else { - ctx.RoutePath = req.URL.RawPath + chiCtx.RoutePath = req.URL.RawPath } next.ServeHTTP(resp, req) }) diff --git a/routers/init.go b/routers/init.go index e7aa765bf0..744feee2f0 100644 --- a/routers/init.go +++ b/routers/init.go @@ -213,7 +213,7 @@ func NormalRoutes() *web.Router { } r.NotFound(func(w http.ResponseWriter, req *http.Request) { - routing.UpdateFuncInfo(req.Context(), routing.GetFuncInfo(http.NotFound, "GlobalNotFound")) + defer routing.RecordFuncInfo(req.Context(), routing.GetFuncInfo(http.NotFound, "GlobalNotFound"))() http.NotFound(w, req) }) return r diff --git a/routers/web/admin/admin.go b/routers/web/admin/admin.go index 3902a1efb1..0cd13acf60 100644 --- a/routers/web/admin/admin.go +++ b/routers/web/admin/admin.go @@ -37,6 +37,7 @@ const ( tplSelfCheck templates.TplName = "admin/self_check" tplCron templates.TplName = "admin/cron" tplQueue templates.TplName = "admin/queue" + tplPerfTrace templates.TplName = "admin/perftrace" tplStacktrace templates.TplName = "admin/stacktrace" tplQueueManage templates.TplName = "admin/queue_manage" tplStats templates.TplName = "admin/stats" diff --git a/routers/web/admin/diagnosis.go b/routers/web/admin/diagnosis.go index 020554a35a..d040dbe0ba 100644 --- a/routers/web/admin/diagnosis.go +++ b/routers/web/admin/diagnosis.go @@ -10,13 +10,15 @@ import ( "time" "code.gitea.io/gitea/modules/httplib" + "code.gitea.io/gitea/modules/tailmsg" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/context" ) func MonitorDiagnosis(ctx *context.Context) { seconds := ctx.FormInt64("seconds") - if seconds <= 5 { - seconds = 5 + if seconds <= 1 { + seconds = 1 } if seconds > 300 { seconds = 300 @@ -65,4 +67,16 @@ func MonitorDiagnosis(ctx *context.Context) { return } _ = pprof.Lookup("heap").WriteTo(f, 0) + + f, err = zipWriter.CreateHeader(&zip.FileHeader{Name: "perftrace.txt", Method: zip.Deflate, Modified: time.Now()}) + if err != nil { + ctx.ServerError("Failed to create zip file", err) + return + } + for _, record := range tailmsg.GetManager().GetTraceRecorder().GetRecords() { + _, _ = f.Write(util.UnsafeStringToBytes(record.Time.Format(time.RFC3339))) + _, _ = f.Write([]byte(" ")) + _, _ = f.Write(util.UnsafeStringToBytes((record.Content))) + _, _ = f.Write([]byte("\n\n")) + } } diff --git a/routers/web/admin/perftrace.go b/routers/web/admin/perftrace.go new file mode 100644 index 0000000000..51ee57da10 --- /dev/null +++ b/routers/web/admin/perftrace.go @@ -0,0 +1,18 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package admin + +import ( + "net/http" + + "code.gitea.io/gitea/modules/tailmsg" + "code.gitea.io/gitea/services/context" +) + +func PerfTrace(ctx *context.Context) { + monitorTraceCommon(ctx) + ctx.Data["PageIsAdminMonitorPerfTrace"] = true + ctx.Data["PerfTraceRecords"] = tailmsg.GetManager().GetTraceRecorder().GetRecords() + ctx.HTML(http.StatusOK, tplPerfTrace) +} diff --git a/routers/web/admin/stacktrace.go b/routers/web/admin/stacktrace.go index ff751be621..2b8c2fb4af 100644 --- a/routers/web/admin/stacktrace.go +++ b/routers/web/admin/stacktrace.go @@ -12,10 +12,17 @@ import ( "code.gitea.io/gitea/services/context" ) +func monitorTraceCommon(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("admin.monitor") + ctx.Data["PageIsAdminMonitorTrace"] = true + // Hide the performance trace tab in production, because it shows a lot of SQLs and is not that useful for end users. + // To avoid confusing end users, do not let them know this tab. End users should "download diagnosis report" instead. + ctx.Data["ShowAdminPerformanceTraceTab"] = !setting.IsProd +} + // Stacktrace show admin monitor goroutines page func Stacktrace(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("admin.monitor") - ctx.Data["PageIsAdminMonitorStacktrace"] = true + monitorTraceCommon(ctx) ctx.Data["GoroutineCount"] = runtime.NumGoroutine() diff --git a/routers/web/auth/linkaccount.go b/routers/web/auth/linkaccount.go index 147d8d3802..9525e19554 100644 --- a/routers/web/auth/linkaccount.go +++ b/routers/web/auth/linkaccount.go @@ -29,6 +29,7 @@ var tplLinkAccount templates.TplName = "user/auth/link_account" // LinkAccount shows the page where the user can decide to login or create a new account func LinkAccount(ctx *context.Context) { + // FIXME: these common template variables should be prepared in one common function, but not just copy-paste again and again. ctx.Data["DisablePassword"] = !setting.Service.RequireExternalRegistrationPassword || setting.Service.AllowOnlyExternalRegistration ctx.Data["Title"] = ctx.Tr("link_account") ctx.Data["LinkAccountMode"] = true @@ -43,6 +44,7 @@ func LinkAccount(ctx *context.Context) { ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration + ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["ShowRegistrationButton"] = false // use this to set the right link into the signIn and signUp templates in the link_account template @@ -50,6 +52,11 @@ func LinkAccount(ctx *context.Context) { ctx.Data["SignUpLink"] = setting.AppSubURL + "/user/link_account_signup" gothUser, ok := ctx.Session.Get("linkAccountGothUser").(goth.User) + + // If you'd like to quickly debug the "link account" page layout, just uncomment the blow line + // Don't worry, when the below line exists, the lint won't pass: ineffectual assignment to gothUser (ineffassign) + // gothUser, ok = goth.User{Email: "invalid-email", Name: "."}, true // intentionally use invalid data to avoid pass the registration check + if !ok { // no account in session, so just redirect to the login page, then the user could restart the process ctx.Redirect(setting.AppSubURL + "/user/login") @@ -135,6 +142,8 @@ func LinkAccountPostSignIn(ctx *context.Context) { ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration + ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration + ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["ShowRegistrationButton"] = false // use this to set the right link into the signIn and signUp templates in the link_account template @@ -223,6 +232,8 @@ func LinkAccountPostRegister(ctx *context.Context) { ctx.Data["McaptchaURL"] = setting.Service.McaptchaURL ctx.Data["CfTurnstileSitekey"] = setting.Service.CfTurnstileSitekey ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration + ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration + ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["ShowRegistrationButton"] = false // use this to set the right link into the signIn and signUp templates in the link_account template diff --git a/routers/web/base.go b/routers/web/base.go index aa0b43c16a..abe11593f7 100644 --- a/routers/web/base.go +++ b/routers/web/base.go @@ -34,7 +34,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } - routing.UpdateFuncInfo(req.Context(), funcInfo) + defer routing.RecordFuncInfo(req.Context(), funcInfo)() rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/") rPath = util.PathJoinRelX(rPath) @@ -65,7 +65,7 @@ func storageHandler(storageSetting *setting.Storage, prefix string, objStore sto http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } - routing.UpdateFuncInfo(req.Context(), funcInfo) + defer routing.RecordFuncInfo(req.Context(), funcInfo)() rPath := strings.TrimPrefix(req.URL.Path, "/"+prefix+"/") rPath = util.PathJoinRelX(rPath) diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 9a18ca5305..e5d83960b8 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -850,7 +850,7 @@ func Run(ctx *context_module.Context) { inputs := make(map[string]any) if workflowDispatch := workflow.WorkflowDispatchConfig(); workflowDispatch != nil { for name, config := range workflowDispatch.Inputs { - value := ctx.Req.PostForm.Get(name) + value := ctx.Req.PostFormValue(name) if config.Type == "boolean" { // https://www.w3.org/TR/html401/interact/forms.html // https://stackoverflow.com/questions/11424037/do-checkbox-inputs-only-post-data-if-theyre-checked diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index 5011cbfc77..8747526f72 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -37,7 +37,6 @@ const ( // Branches render repository branch page func Branches(ctx *context.Context) { ctx.Data["Title"] = "Branches" - ctx.Data["IsRepoToolbarBranches"] = true ctx.Data["AllowsPulls"] = ctx.Repo.Repository.AllowsPulls(ctx) ctx.Data["IsWriter"] = ctx.Repo.CanWrite(unit.TypeCode) ctx.Data["IsMirror"] = ctx.Repo.Repository.IsMirror diff --git a/routers/web/repo/code_frequency.go b/routers/web/repo/code_frequency.go index 6572adce74..e212d3b60c 100644 --- a/routers/web/repo/code_frequency.go +++ b/routers/web/repo/code_frequency.go @@ -29,7 +29,7 @@ func CodeFrequency(ctx *context.Context) { // CodeFrequencyData returns JSON of code frequency data func CodeFrequencyData(ctx *context.Context) { - if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { + if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil { if errors.Is(err, contributors_service.ErrAwaitGeneration) { ctx.Status(http.StatusAccepted) return diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go index bad8ca56d1..8ffda8ae0a 100644 --- a/routers/web/repo/commit.go +++ b/routers/web/repo/commit.go @@ -62,11 +62,7 @@ func Commits(ctx *context.Context) { } ctx.Data["PageIsViewCode"] = true - commitsCount, err := ctx.Repo.GetCommitsCount() - if err != nil { - ctx.ServerError("GetCommitsCount", err) - return - } + commitsCount := ctx.Repo.CommitsCount page := ctx.FormInt("page") if page <= 1 { @@ -129,12 +125,6 @@ func Graph(ctx *context.Context) { ctx.Data["SelectedBranches"] = realBranches files := ctx.FormStrings("file") - commitsCount, err := ctx.Repo.GetCommitsCount() - if err != nil { - ctx.ServerError("GetCommitsCount", err) - return - } - graphCommitsCount, err := ctx.Repo.GetCommitGraphsCount(ctx, hidePRRefs, realBranches, files) if err != nil { log.Warn("GetCommitGraphsCount error for generate graph exclude prs: %t branches: %s in %-v, Will Ignore branches and try again. Underlying Error: %v", hidePRRefs, branches, ctx.Repo.Repository, err) @@ -171,7 +161,6 @@ func Graph(ctx *context.Context) { ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name - ctx.Data["CommitCount"] = commitsCount paginator := context.NewPagination(int(graphCommitsCount), setting.UI.GraphMaxCommitNum, page, 5) paginator.AddParamFromRequest(ctx.Req) diff --git a/routers/web/repo/contributors.go b/routers/web/repo/contributors.go index e9c0919955..93dec1f350 100644 --- a/routers/web/repo/contributors.go +++ b/routers/web/repo/contributors.go @@ -26,7 +26,7 @@ func Contributors(ctx *context.Context) { // ContributorsData renders JSON of contributors along with their weekly commit statistics func ContributorsData(ctx *context.Context) { - if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { + if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil { if errors.Is(err, contributors_service.ErrAwaitGeneration) { ctx.Status(http.StatusAccepted) return diff --git a/routers/web/repo/issue_dependency.go b/routers/web/repo/issue_dependency.go index f1d133edb0..0f6787386d 100644 --- a/routers/web/repo/issue_dependency.go +++ b/routers/web/repo/issue_dependency.go @@ -109,7 +109,7 @@ func RemoveDependency(ctx *context.Context) { } // Dependency Type - depTypeStr := ctx.Req.PostForm.Get("dependencyType") + depTypeStr := ctx.Req.PostFormValue("dependencyType") var depType issues_model.DependencyType diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go index 8a613e2c7e..486c2e35a2 100644 --- a/routers/web/repo/issue_label_test.go +++ b/routers/web/repo/issue_label_test.go @@ -38,7 +38,7 @@ func TestInitializeLabels(t *testing.T) { contexttest.LoadRepo(t, ctx, 2) web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"}) InitializeLabels(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ RepoID: 2, Name: "enhancement", @@ -84,7 +84,7 @@ func TestNewLabel(t *testing.T) { Color: "#abcdef", }) NewLabel(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ Name: "newlabel", Color: "#abcdef", @@ -104,7 +104,7 @@ func TestUpdateLabel(t *testing.T) { IsArchived: true, }) UpdateLabel(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ ID: 2, Name: "newnameforlabel", @@ -120,7 +120,7 @@ func TestDeleteLabel(t *testing.T) { contexttest.LoadRepo(t, ctx, 1) ctx.Req.Form.Set("id", "2") DeleteLabel(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) unittest.AssertNotExistsBean(t, &issues_model.Label{ID: 2}) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2}) assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg) @@ -134,7 +134,7 @@ func TestUpdateIssueLabel_Clear(t *testing.T) { ctx.Req.Form.Set("issue_ids", "1,3") ctx.Req.Form.Set("action", "clear") UpdateIssueLabel(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 1}) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 3}) unittest.CheckConsistencyFor(t, &issues_model.Label{}) @@ -160,7 +160,7 @@ func TestUpdateIssueLabel_Toggle(t *testing.T) { ctx.Req.Form.Set("action", testCase.Action) ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID))) UpdateIssueLabel(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) for _, issueID := range testCase.IssueIDs { if testCase.ExpectedAdd { unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID}) diff --git a/routers/web/repo/issue_timetrack.go b/routers/web/repo/issue_timetrack.go index 36e931a48f..134ded82d1 100644 --- a/routers/web/repo/issue_timetrack.go +++ b/routers/web/repo/issue_timetrack.go @@ -81,7 +81,7 @@ func DeleteTime(c *context.Context) { return } - c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time))) + c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToHours(t.Time))) c.JSONRedirect("") } diff --git a/routers/web/repo/issue_watch.go b/routers/web/repo/issue_watch.go index a2a4be1758..e7d55e5555 100644 --- a/routers/web/repo/issue_watch.go +++ b/routers/web/repo/issue_watch.go @@ -46,7 +46,7 @@ func IssueWatch(ctx *context.Context) { return } - watch, err := strconv.ParseBool(ctx.Req.PostForm.Get("watch")) + watch, err := strconv.ParseBool(ctx.Req.PostFormValue("watch")) if err != nil { ctx.ServerError("watch is not bool", err) return diff --git a/routers/web/repo/recent_commits.go b/routers/web/repo/recent_commits.go index dc72081900..228eb0dbac 100644 --- a/routers/web/repo/recent_commits.go +++ b/routers/web/repo/recent_commits.go @@ -29,7 +29,7 @@ func RecentCommits(ctx *context.Context) { // RecentCommitsData returns JSON of recent commits data func RecentCommitsData(ctx *context.Context) { - if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil { + if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch); err != nil { if errors.Is(err, contributors_service.ErrAwaitGeneration) { ctx.Status(http.StatusAccepted) return diff --git a/routers/web/repo/search.go b/routers/web/repo/search.go index d589586dad..bbbe5c1081 100644 --- a/routers/web/repo/search.go +++ b/routers/web/repo/search.go @@ -67,10 +67,11 @@ func Search(ctx *context.Context) { ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable(ctx) } } else { + searchRefName := git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch) // BranchName should be default branch or the first existing branch res, err := git.GrepSearch(ctx, ctx.Repo.GitRepo, prepareSearch.Keyword, git.GrepOptions{ ContextLineNumber: 1, IsFuzzy: prepareSearch.IsFuzzy, - RefName: git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch).String(), // BranchName should be default branch or the first existing branch + RefName: searchRefName.String(), PathspecList: indexSettingToGitGrepPathspecList(), }) if err != nil { @@ -78,6 +79,11 @@ func Search(ctx *context.Context) { ctx.ServerError("GrepSearch", err) return } + commitID, err := ctx.Repo.GitRepo.GetRefCommitID(searchRefName.String()) + if err != nil { + ctx.ServerError("GetRefCommitID", err) + return + } total = len(res) pageStart := min((page-1)*setting.UI.RepoSearchPagingNum, len(res)) pageEnd := min(page*setting.UI.RepoSearchPagingNum, len(res)) @@ -86,7 +92,7 @@ func Search(ctx *context.Context) { searchResults = append(searchResults, &code_indexer.Result{ RepoID: ctx.Repo.Repository.ID, Filename: r.Filename, - CommitID: ctx.Repo.CommitID, + CommitID: commitID, // UpdatedUnix: not supported yet // Language: not supported yet // Color: not supported yet diff --git a/routers/web/repo/setting/protected_branch.go b/routers/web/repo/setting/protected_branch.go index 022a24a9ad..06a9e69507 100644 --- a/routers/web/repo/setting/protected_branch.go +++ b/routers/web/repo/setting/protected_branch.go @@ -4,6 +4,7 @@ package setting import ( + "errors" "fmt" "net/http" "net/url" @@ -14,6 +15,7 @@ import ( "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/web" @@ -351,9 +353,15 @@ func RenameBranchPost(ctx *context.Context) { msg, err := repository.RenameBranch(ctx, ctx.Repo.Repository, ctx.Doer, ctx.Repo.GitRepo, form.From, form.To) if err != nil { switch { + case repo_model.IsErrUserDoesNotHaveAccessToRepo(err): + ctx.Flash.Error(ctx.Tr("repo.branch.rename_default_or_protected_branch_error")) + ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink)) case git_model.IsErrBranchAlreadyExists(err): ctx.Flash.Error(ctx.Tr("repo.branch.branch_already_exists", form.To)) ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink)) + case errors.Is(err, git_model.ErrBranchIsProtected): + ctx.Flash.Error(ctx.Tr("repo.branch.rename_protected_branch_failed")) + ctx.Redirect(fmt.Sprintf("%s/branches", ctx.Repo.RepoLink)) default: ctx.ServerError("RenameBranch", err) } diff --git a/routers/web/repo/setting/settings_test.go b/routers/web/repo/setting/settings_test.go index 09586cc68d..ad33dac514 100644 --- a/routers/web/repo/setting/settings_test.go +++ b/routers/web/repo/setting/settings_test.go @@ -54,7 +54,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) { } web.SetForm(ctx, &addKeyForm) DeployKeysPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{ Name: addKeyForm.Title, @@ -84,7 +84,7 @@ func TestAddReadWriteOnlyDeployKey(t *testing.T) { } web.SetForm(ctx, &addKeyForm) DeployKeysPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{ Name: addKeyForm.Title, @@ -121,7 +121,7 @@ func TestCollaborationPost(t *testing.T) { CollaborationPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) exists, err := repo_model.IsCollaborator(ctx, re.ID, 4) assert.NoError(t, err) @@ -147,7 +147,7 @@ func TestCollaborationPost_InactiveUser(t *testing.T) { CollaborationPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } @@ -179,7 +179,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) { CollaborationPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) exists, err := repo_model.IsCollaborator(ctx, re.ID, 4) assert.NoError(t, err) @@ -188,7 +188,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) { // Try adding the same collaborator again CollaborationPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } @@ -210,7 +210,7 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) { CollaborationPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } @@ -250,7 +250,7 @@ func TestAddTeamPost(t *testing.T) { AddTeamPost(ctx) assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID)) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.Empty(t, ctx.Flash.ErrorMsg) } @@ -290,7 +290,7 @@ func TestAddTeamPost_NotAllowed(t *testing.T) { AddTeamPost(ctx) assert.False(t, repo_service.HasRepository(db.DefaultContext, team, re.ID)) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } @@ -331,7 +331,7 @@ func TestAddTeamPost_AddTeamTwice(t *testing.T) { AddTeamPost(ctx) assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID)) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } @@ -364,7 +364,7 @@ func TestAddTeamPost_NonExistentTeam(t *testing.T) { ctx.Repo = repo AddTeamPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.NotEmpty(t, ctx.Flash.ErrorMsg) } diff --git a/routers/web/repo/setting/webhook.go b/routers/web/repo/setting/webhook.go index 1b0ba83af4..ce67ea3c01 100644 --- a/routers/web/repo/setting/webhook.go +++ b/routers/web/repo/setting/webhook.go @@ -654,6 +654,8 @@ func TestWebhook(ctx *context.Context) { } // Grab latest commit or fake one if it's empty repository. + // Note: in old code, the "ctx.Repo.Commit" is the last commit of the default branch. + // New code doesn't set that commit, so it always uses the fake commit to test webhook. commit := ctx.Repo.Commit if commit == nil { ghost := user_model.NewGhostUser() diff --git a/routers/web/repo/view_home.go b/routers/web/repo/view_home.go index ff2ee3c7ec..d141df181c 100644 --- a/routers/web/repo/view_home.go +++ b/routers/web/repo/view_home.go @@ -215,10 +215,28 @@ func prepareRecentlyPushedNewBranches(ctx *context.Context) { if !opts.Repo.IsMirror && !opts.BaseRepo.IsMirror && opts.BaseRepo.UnitEnabled(ctx, unit_model.TypePullRequests) && baseRepoPerm.CanRead(unit_model.TypePullRequests) { - ctx.Data["RecentlyPushedNewBranches"], err = git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts) + var finalBranches []*git_model.RecentlyPushedNewBranch + branches, err := git_model.FindRecentlyPushedNewBranches(ctx, ctx.Doer, opts) if err != nil { log.Error("FindRecentlyPushedNewBranches failed: %v", err) } + + for _, branch := range branches { + divergingInfo, err := repo_service.GetBranchDivergingInfo(ctx, + branch.BranchRepo, branch.BranchName, // "base" repo for diverging info + opts.BaseRepo, opts.BaseRepo.DefaultBranch, // "head" repo for diverging info + ) + if err != nil { + log.Error("GetBranchDivergingInfo failed: %v", err) + continue + } + branchRepoHasNewCommits := divergingInfo.BaseHasNewCommits + baseRepoCommitsBehind := divergingInfo.HeadCommitsBehind + if branchRepoHasNewCommits || baseRepoCommitsBehind > 0 { + finalBranches = append(finalBranches, branch) + } + } + ctx.Data["RecentlyPushedNewBranches"] = finalBranches } } } diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index c31f29164b..99114c93e0 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -82,7 +82,7 @@ func TestWiki(t *testing.T) { ctx.SetPathParam("*", "Home") contexttest.LoadRepo(t, ctx, 1) Wiki(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, "Home", ctx.Data["Title"]) assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"]) @@ -90,7 +90,7 @@ func TestWiki(t *testing.T) { ctx.SetPathParam("*", "jpeg.jpg") contexttest.LoadRepo(t, ctx, 1) Wiki(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assert.Equal(t, "/user2/repo1/wiki/raw/jpeg.jpg", ctx.Resp.Header().Get("Location")) } @@ -100,7 +100,7 @@ func TestWikiPages(t *testing.T) { ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages") contexttest.LoadRepo(t, ctx, 1) WikiPages(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"]) } @@ -111,7 +111,7 @@ func TestNewWiki(t *testing.T) { contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) NewWiki(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, ctx.Tr("repo.wiki.new_page"), ctx.Data["Title"]) } @@ -131,7 +131,7 @@ func TestNewWikiPost(t *testing.T) { Message: message, }) NewWikiPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)) assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))) } @@ -149,7 +149,7 @@ func TestNewWikiPost_ReservedName(t *testing.T) { Message: message, }) NewWikiPost(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg) assertWikiNotExists(t, ctx.Repo.Repository, "_edit") } @@ -162,7 +162,7 @@ func TestEditWiki(t *testing.T) { contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) EditWiki(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, "Home", ctx.Data["Title"]) assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"]) @@ -171,7 +171,7 @@ func TestEditWiki(t *testing.T) { contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) EditWiki(ctx) - assert.EqualValues(t, http.StatusForbidden, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusForbidden, ctx.Resp.WrittenStatus()) } func TestEditWikiPost(t *testing.T) { @@ -190,7 +190,7 @@ func TestEditWikiPost(t *testing.T) { Message: message, }) EditWikiPost(ctx) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)) assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))) if title != "Home" { @@ -206,7 +206,7 @@ func TestDeleteWikiPagePost(t *testing.T) { contexttest.LoadUser(t, ctx, 2) contexttest.LoadRepo(t, ctx, 1) DeleteWikiPagePost(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assertWikiNotExists(t, ctx.Repo.Repository, "Home") } @@ -228,9 +228,9 @@ func TestWikiRaw(t *testing.T) { contexttest.LoadRepo(t, ctx, 1) WikiRaw(ctx) if filetype == "" { - assert.EqualValues(t, http.StatusNotFound, ctx.Resp.Status(), "filepath: %s", filepath) + assert.EqualValues(t, http.StatusNotFound, ctx.Resp.WrittenStatus(), "filepath: %s", filepath) } else { - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status(), "filepath: %s", filepath) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus(), "filepath: %s", filepath) assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath) } } diff --git a/routers/web/user/home.go b/routers/web/user/home.go index c79648a455..235a7c6f39 100644 --- a/routers/web/user/home.go +++ b/routers/web/user/home.go @@ -576,17 +576,9 @@ func buildIssueOverview(ctx *context.Context, unitType unit.Type) { // ------------------------------- // Fill stats to post to ctx.Data. // ------------------------------- - issueStats, err := getUserIssueStats(ctx, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy( + issueStats, err := getUserIssueStats(ctx, ctxUser, filterMode, issue_indexer.ToSearchOptions(keyword, opts).Copy( func(o *issue_indexer.SearchOptions) { o.IsFuzzyKeyword = isFuzzy - // If the doer is the same as the context user, which means the doer is viewing his own dashboard, - // it's not enough to show the repos that the doer owns or has been explicitly granted access to, - // because the doer may create issues or be mentioned in any public repo. - // So we need search issues in all public repos. - o.AllPublic = ctx.Doer.ID == ctxUser.ID - o.MentionID = nil - o.ReviewRequestedID = nil - o.ReviewedID = nil }, )) if err != nil { @@ -775,10 +767,19 @@ func UsernameSubRoute(ctx *context.Context) { } } -func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) { +func getUserIssueStats(ctx *context.Context, ctxUser *user_model.User, filterMode int, opts *issue_indexer.SearchOptions) (ret *issues_model.IssueStats, err error) { ret = &issues_model.IssueStats{} doerID := ctx.Doer.ID + opts = opts.Copy(func(o *issue_indexer.SearchOptions) { + // If the doer is the same as the context user, which means the doer is viewing his own dashboard, + // it's not enough to show the repos that the doer owns or has been explicitly granted access to, + // because the doer may create issues or be mentioned in any public repo. + // So we need search issues in all public repos. + o.AllPublic = doerID == ctxUser.ID + }) + + // Open/Closed are for the tabs of the issue list { openClosedOpts := opts.Copy() switch filterMode { @@ -809,6 +810,15 @@ func getUserIssueStats(ctx *context.Context, filterMode int, opts *issue_indexer } } + // Below stats are for the left sidebar + opts = opts.Copy(func(o *issue_indexer.SearchOptions) { + o.AssigneeID = nil + o.PosterID = nil + o.MentionID = nil + o.ReviewRequestedID = nil + o.ReviewedID = nil + }) + ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AllPublic = false })) if err != nil { return nil, err diff --git a/routers/web/user/home_test.go b/routers/web/user/home_test.go index 51246551ea..b2c8ad98ba 100644 --- a/routers/web/user/home_test.go +++ b/routers/web/user/home_test.go @@ -45,7 +45,7 @@ func TestArchivedIssues(t *testing.T) { Issues(ctx) // Assert: One Issue (ID 30) from one Repo (ID 50) is retrieved, while nothing from archived Repo 51 is retrieved - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.Len(t, ctx.Data["Issues"], 1) } @@ -58,7 +58,7 @@ func TestIssues(t *testing.T) { contexttest.LoadUser(t, ctx, 2) ctx.Req.Form.Set("state", "closed") Issues(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, true, ctx.Data["IsShowClosed"]) assert.Len(t, ctx.Data["Issues"], 1) @@ -72,7 +72,7 @@ func TestPulls(t *testing.T) { contexttest.LoadUser(t, ctx, 2) ctx.Req.Form.Set("state", "open") Pulls(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.Len(t, ctx.Data["Issues"], 5) } @@ -87,7 +87,7 @@ func TestMilestones(t *testing.T) { ctx.Req.Form.Set("state", "closed") ctx.Req.Form.Set("sort", "furthestduedate") Milestones(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"]) assert.EqualValues(t, true, ctx.Data["IsShowClosed"]) assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"]) @@ -107,7 +107,7 @@ func TestMilestonesForSpecificRepo(t *testing.T) { ctx.Req.Form.Set("state", "closed") ctx.Req.Form.Set("sort", "furthestduedate") Milestones(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus()) assert.EqualValues(t, map[int64]int64{1: 1}, ctx.Data["Counts"]) assert.EqualValues(t, true, ctx.Data["IsShowClosed"]) assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"]) diff --git a/routers/web/user/setting/account_test.go b/routers/web/user/setting/account_test.go index 9fdc5e4d53..13caa33771 100644 --- a/routers/web/user/setting/account_test.go +++ b/routers/web/user/setting/account_test.go @@ -95,7 +95,7 @@ func TestChangePassword(t *testing.T) { AccountPost(ctx) assert.Contains(t, ctx.Flash.ErrorMsg, req.Message) - assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) + assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus()) }) } } diff --git a/routers/web/web.go b/routers/web/web.go index f5e21a7070..02a3d39670 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -720,6 +720,7 @@ func registerRoutes(m *web.Router) { m.Group("/monitor", func() { m.Get("/stats", admin.MonitorStats) m.Get("/cron", admin.CronTasks) + m.Get("/perftrace", admin.PerfTrace) m.Get("/stacktrace", admin.Stacktrace) m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel) m.Get("/queue", admin.Queues) @@ -1156,7 +1157,7 @@ func registerRoutes(m *web.Router) { m.Post("/cancel", repo.MigrateCancelPost) }) }, - reqSignIn, context.RepoAssignment, reqRepoAdmin, context.RepoRef(), + reqSignIn, context.RepoAssignment, reqRepoAdmin, ctxDataSet("PageIsRepoSettings", true, "LFSStartServer", setting.LFS.StartServer), ) // end "/{username}/{reponame}/settings" @@ -1342,7 +1343,7 @@ func registerRoutes(m *web.Router) { m.Group("/{username}/{reponame}", func() { // repo tags m.Group("/tags", func() { - m.Get("", repo.TagsList) + m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.TagsList) m.Get(".rss", feedEnabled, repo.TagsListFeedRSS) m.Get(".atom", feedEnabled, repo.TagsListFeedAtom) m.Get("/list", repo.GetTagList) @@ -1523,7 +1524,7 @@ func registerRoutes(m *web.Router) { m.Group("/activity_author_data", func() { m.Get("", repo.ActivityAuthors) m.Get("/{period}", repo.ActivityAuthors) - }, context.RepoRef(), repo.MustBeNotEmpty) + }, repo.MustBeNotEmpty) m.Group("/archive", func() { m.Get("/*", repo.Download) @@ -1532,8 +1533,8 @@ func registerRoutes(m *web.Router) { m.Group("/branches", func() { m.Get("/list", repo.GetBranchesList) - m.Get("", repo.Branches) - }, repo.MustBeNotEmpty, context.RepoRef()) + m.Get("", context.RepoRefByDefaultBranch() /* for the "commits" tab */, repo.Branches) + }, repo.MustBeNotEmpty) m.Group("/media", func() { m.Get("/blob/{sha}", repo.DownloadByIDOrLFS) @@ -1577,8 +1578,10 @@ func registerRoutes(m *web.Router) { m.Get("/graph", repo.Graph) m.Get("/commit/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.SetDiffViewStyle, repo.SetWhitespaceBehavior, repo.Diff) m.Get("/commit/{sha:([a-f0-9]{7,64})$}/load-branches-and-tags", repo.LoadBranchesAndTags) - m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick) - }, repo.MustBeNotEmpty, context.RepoRef()) + + // FIXME: this route `/cherry-pick/{sha}` doesn't seem useful or right, the new code always uses `/_cherrypick/` which could handle branch name correctly + m.Get("/cherry-pick/{sha:([a-f0-9]{7,64})$}", repo.SetEditorconfigIfExists, context.RepoRefByDefaultBranch(), repo.CherryPick) + }, repo.MustBeNotEmpty) m.Get("/rss/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeed) m.Get("/atom/branch/*", context.RepoRefByType(git.RefTypeBranch), feedEnabled, feed.RenderBranchFeed) @@ -1632,7 +1635,7 @@ func registerRoutes(m *web.Router) { m.NotFound(func(w http.ResponseWriter, req *http.Request) { ctx := context.GetWebContext(req) - routing.UpdateFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound")) + defer routing.RecordFuncInfo(ctx, routing.GetFuncInfo(ctx.NotFound, "WebNotFound"))() ctx.NotFound("", nil) }) } diff --git a/services/context/access_log.go b/services/context/access_log.go index 0926748ac5..001d93a362 100644 --- a/services/context/access_log.go +++ b/services/context/access_log.go @@ -18,13 +18,14 @@ import ( "code.gitea.io/gitea/modules/web/middleware" ) -type routerLoggerOptions struct { - req *http.Request +type accessLoggerTmplData struct { Identity *string Start *time.Time - ResponseWriter http.ResponseWriter - Ctx map[string]any - RequestID *string + ResponseWriter struct { + Status, Size int + } + Ctx map[string]any + RequestID *string } const keyOfRequestIDInTemplate = ".RequestID" @@ -51,51 +52,65 @@ func parseRequestIDFromRequestHeader(req *http.Request) string { return requestID } +type accessLogRecorder struct { + logger log.BaseLogger + logTemplate *template.Template + needRequestID bool +} + +func (lr *accessLogRecorder) record(start time.Time, respWriter ResponseWriter, req *http.Request) { + var requestID string + if lr.needRequestID { + requestID = parseRequestIDFromRequestHeader(req) + } + + reqHost, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + reqHost = req.RemoteAddr + } + + identity := "-" + data := middleware.GetContextData(req.Context()) + if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok { + identity = signedUser.Name + } + buf := bytes.NewBuffer([]byte{}) + tmplData := accessLoggerTmplData{ + Identity: &identity, + Start: &start, + Ctx: map[string]any{ + "RemoteAddr": req.RemoteAddr, + "RemoteHost": reqHost, + "Req": req, + }, + RequestID: &requestID, + } + tmplData.ResponseWriter.Status = respWriter.WrittenStatus() + tmplData.ResponseWriter.Size = respWriter.WrittenSize() + err = lr.logTemplate.Execute(buf, tmplData) + if err != nil { + log.Error("Could not execute access logger template: %v", err.Error()) + } + + lr.logger.Log(1, log.INFO, "%s", buf.String()) +} + +func newAccessLogRecorder() *accessLogRecorder { + return &accessLogRecorder{ + logger: log.GetLogger("access"), + logTemplate: template.Must(template.New("log").Parse(setting.Log.AccessLogTemplate)), + needRequestID: len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate), + } +} + // AccessLogger returns a middleware to log access logger func AccessLogger() func(http.Handler) http.Handler { - logger := log.GetLogger("access") - needRequestID := len(setting.Log.RequestIDHeaders) > 0 && strings.Contains(setting.Log.AccessLogTemplate, keyOfRequestIDInTemplate) - logTemplate, _ := template.New("log").Parse(setting.Log.AccessLogTemplate) + recorder := newAccessLogRecorder() return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { start := time.Now() - - var requestID string - if needRequestID { - requestID = parseRequestIDFromRequestHeader(req) - } - - reqHost, _, err := net.SplitHostPort(req.RemoteAddr) - if err != nil { - reqHost = req.RemoteAddr - } - next.ServeHTTP(w, req) - rw := w.(ResponseWriter) - - identity := "-" - data := middleware.GetContextData(req.Context()) - if signedUser, ok := data[middleware.ContextDataKeySignedUser].(*user_model.User); ok { - identity = signedUser.Name - } - buf := bytes.NewBuffer([]byte{}) - err = logTemplate.Execute(buf, routerLoggerOptions{ - req: req, - Identity: &identity, - Start: &start, - ResponseWriter: rw, - Ctx: map[string]any{ - "RemoteAddr": req.RemoteAddr, - "RemoteHost": reqHost, - "Req": req, - }, - RequestID: &requestID, - }) - if err != nil { - log.Error("Could not execute access logger template: %v", err.Error()) - } - - logger.Info("%s", buf.String()) + recorder.record(start, w.(ResponseWriter), req) }) } } diff --git a/services/context/access_log_test.go b/services/context/access_log_test.go new file mode 100644 index 0000000000..bd3e47e0cc --- /dev/null +++ b/services/context/access_log_test.go @@ -0,0 +1,71 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package context + +import ( + "fmt" + "net/http" + "net/url" + "testing" + "time" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "github.com/stretchr/testify/assert" +) + +type testAccessLoggerMock struct { + logs []string +} + +func (t *testAccessLoggerMock) Log(skip int, level log.Level, format string, v ...any) { + t.logs = append(t.logs, fmt.Sprintf(format, v...)) +} + +func (t *testAccessLoggerMock) GetLevel() log.Level { + return log.INFO +} + +type testAccessLoggerResponseWriterMock struct{} + +func (t testAccessLoggerResponseWriterMock) Header() http.Header { + return nil +} + +func (t testAccessLoggerResponseWriterMock) Before(f func(ResponseWriter)) {} + +func (t testAccessLoggerResponseWriterMock) WriteHeader(statusCode int) {} + +func (t testAccessLoggerResponseWriterMock) Write(bytes []byte) (int, error) { + return 0, nil +} + +func (t testAccessLoggerResponseWriterMock) Flush() {} + +func (t testAccessLoggerResponseWriterMock) WrittenStatus() int { + return http.StatusOK +} + +func (t testAccessLoggerResponseWriterMock) WrittenSize() int { + return 123123 +} + +func TestAccessLogger(t *testing.T) { + setting.Log.AccessLogTemplate = `{{.Ctx.RemoteHost}} - {{.Identity}} {{.Start.Format "[02/Jan/2006:15:04:05 -0700]" }} "{{.Ctx.Req.Method}} {{.Ctx.Req.URL.RequestURI}} {{.Ctx.Req.Proto}}" {{.ResponseWriter.Status}} {{.ResponseWriter.Size}} "{{.Ctx.Req.Referer}}" "{{.Ctx.Req.UserAgent}}"` + recorder := newAccessLogRecorder() + mockLogger := &testAccessLoggerMock{} + recorder.logger = mockLogger + req := &http.Request{ + RemoteAddr: "remote-addr", + Method: "GET", + Proto: "https", + URL: &url.URL{Path: "/path"}, + } + req.Header = http.Header{} + req.Header.Add("Referer", "referer") + req.Header.Add("User-Agent", "user-agent") + recorder.record(time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC), &testAccessLoggerResponseWriterMock{}, req) + assert.Equal(t, []string{`remote-addr - - [02/Jan/2000:03:04:05 +0000] "GET /path https" 200 123123 "referer" "user-agent"`}, mockLogger.logs) +} diff --git a/services/context/base.go b/services/context/base.go index 7a39353e09..5db84f42a5 100644 --- a/services/context/base.go +++ b/services/context/base.go @@ -4,7 +4,6 @@ package context import ( - "context" "fmt" "html/template" "io" @@ -25,8 +24,7 @@ type BaseContextKeyType struct{} var BaseContextKey BaseContextKeyType type Base struct { - context.Context - reqctx.RequestDataStore + reqctx.RequestContext Resp ResponseWriter Req *http.Request @@ -172,19 +170,19 @@ func (b *Base) TrN(cnt any, key1, keyN string, args ...any) template.HTML { } func NewBaseContext(resp http.ResponseWriter, req *http.Request) *Base { - ds := reqctx.GetRequestDataStore(req.Context()) + reqCtx := reqctx.FromContext(req.Context()) b := &Base{ - Context: req.Context(), - RequestDataStore: ds, - Req: req, - Resp: WrapResponseWriter(resp), - Locale: middleware.Locale(resp, req), - Data: ds.GetData(), + RequestContext: reqCtx, + + Req: req, + Resp: WrapResponseWriter(resp), + Locale: middleware.Locale(resp, req), + Data: reqCtx.GetData(), } b.Req = b.Req.WithContext(b) - ds.SetContextValue(BaseContextKey, b) - ds.SetContextValue(translation.ContextKey, b.Locale) - ds.SetContextValue(httplib.RequestContextKey, b.Req) + reqCtx.SetContextValue(BaseContextKey, b) + reqCtx.SetContextValue(translation.ContextKey, b.Locale) + reqCtx.SetContextValue(httplib.RequestContextKey, b.Req) return b } diff --git a/services/context/repo.go b/services/context/repo.go index ef54b9cee8..6cd70d139b 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -777,6 +777,18 @@ func repoRefFullName(typ git.RefType, shortName string) git.RefName { } } +func RepoRefByDefaultBranch() func(*Context) { + return func(ctx *Context) { + ctx.Repo.RefFullName = git.RefNameFromBranch(ctx.Repo.Repository.DefaultBranch) + ctx.Repo.BranchName = ctx.Repo.Repository.DefaultBranch + ctx.Repo.Commit, _ = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.BranchName) + ctx.Repo.CommitsCount, _ = ctx.Repo.GetCommitsCount() + ctx.Data["RefFullName"] = ctx.Repo.RefFullName + ctx.Data["BranchName"] = ctx.Repo.BranchName + ctx.Data["CommitsCount"] = ctx.Repo.CommitsCount + } +} + // RepoRefByType handles repository reference name for a specific type // of repository reference func RepoRefByType(detectRefType git.RefType) func(*Context) { diff --git a/services/context/response.go b/services/context/response.go index 2f271f211b..c7368ebc6f 100644 --- a/services/context/response.go +++ b/services/context/response.go @@ -11,31 +11,29 @@ import ( // ResponseWriter represents a response writer for HTTP type ResponseWriter interface { - http.ResponseWriter - http.Flusher - web_types.ResponseStatusProvider + http.ResponseWriter // provides Header/Write/WriteHeader + http.Flusher // provides Flush + web_types.ResponseStatusProvider // provides WrittenStatus - Before(func(ResponseWriter)) - - Status() int // used by access logger template - Size() int // used by access logger template + Before(fn func(ResponseWriter)) + WrittenSize() int } -var _ ResponseWriter = &Response{} +var _ ResponseWriter = (*Response)(nil) // Response represents a response type Response struct { http.ResponseWriter written int status int - befores []func(ResponseWriter) + beforeFuncs []func(ResponseWriter) beforeExecuted bool } // Write writes bytes to HTTP endpoint func (r *Response) Write(bs []byte) (int, error) { if !r.beforeExecuted { - for _, before := range r.befores { + for _, before := range r.beforeFuncs { before(r) } r.beforeExecuted = true @@ -51,18 +49,14 @@ func (r *Response) Write(bs []byte) (int, error) { return size, nil } -func (r *Response) Status() int { - return r.status -} - -func (r *Response) Size() int { +func (r *Response) WrittenSize() int { return r.written } // WriteHeader write status code func (r *Response) WriteHeader(statusCode int) { if !r.beforeExecuted { - for _, before := range r.befores { + for _, before := range r.beforeFuncs { before(r) } r.beforeExecuted = true @@ -87,17 +81,13 @@ func (r *Response) WrittenStatus() int { // Before allows for a function to be called before the ResponseWriter has been written to. This is // useful for setting headers or any other operations that must happen before a response has been written. -func (r *Response) Before(f func(ResponseWriter)) { - r.befores = append(r.befores, f) +func (r *Response) Before(fn func(ResponseWriter)) { + r.beforeFuncs = append(r.beforeFuncs, fn) } func WrapResponseWriter(resp http.ResponseWriter) *Response { if v, ok := resp.(*Response); ok { return v } - return &Response{ - ResponseWriter: resp, - status: 0, - befores: make([]func(ResponseWriter), 0), - } + return &Response{ResponseWriter: resp} } diff --git a/services/convert/issue.go b/services/convert/issue.go index e3124efd64..37935accca 100644 --- a/services/convert/issue.go +++ b/services/convert/issue.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" ) func ToIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Issue) *api.Issue { @@ -186,7 +187,7 @@ func ToStopWatches(ctx context.Context, sws []*issues_model.Stopwatch) (api.Stop result = append(result, api.StopWatch{ Created: sw.CreatedUnix.AsTime(), Seconds: sw.Seconds(), - Duration: sw.Duration(), + Duration: util.SecToHours(sw.Seconds()), IssueIndex: issue.Index, IssueTitle: issue.Title, RepoOwnerName: repo.OwnerName, diff --git a/services/convert/issue_comment.go b/services/convert/issue_comment.go index b8527ae233..9ad584a62f 100644 --- a/services/convert/issue_comment.go +++ b/services/convert/issue_comment.go @@ -74,7 +74,7 @@ func ToTimelineComment(ctx context.Context, repo *repo_model.Repository, c *issu c.Content[0] == '|' { // TimeTracking Comments from v1.21 on store the seconds instead of an formatted string // so we check for the "|" delimiter and convert new to legacy format on demand - c.Content = util.SecToTime(c.Content[1:]) + c.Content = util.SecToHours(c.Content[1:]) } if c.Type == issues_model.CommentTypeChangeTimeEstimate { diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index f42686bb71..f046e59678 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -1136,7 +1136,10 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi } else { actualBeforeCommitID := opts.BeforeCommitID if len(actualBeforeCommitID) == 0 { - parentCommit, _ := commit.Parent(0) + parentCommit, err := commit.Parent(0) + if err != nil { + return nil, err + } actualBeforeCommitID = parentCommit.ID.String() } @@ -1145,7 +1148,6 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi AddDynamicArguments(actualBeforeCommitID, opts.AfterCommitID) opts.BeforeCommitID = actualBeforeCommitID - var err error beforeCommit, err = gitRepo.GetCommit(opts.BeforeCommitID) if err != nil { return nil, err diff --git a/services/repository/branch.go b/services/repository/branch.go index 302cfff62e..c80d367bbd 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -26,6 +26,7 @@ import ( "code.gitea.io/gitea/modules/optional" "code.gitea.io/gitea/modules/queue" repo_module "code.gitea.io/gitea/modules/repository" + "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" webhook_module "code.gitea.io/gitea/modules/webhook" @@ -416,6 +417,29 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m return "from_not_exist", nil } + perm, err := access_model.GetUserRepoPermission(ctx, repo, doer) + if err != nil { + return "", err + } + + isDefault := from == repo.DefaultBranch + if isDefault && !perm.IsAdmin() { + return "", repo_model.ErrUserDoesNotHaveAccessToRepo{ + UserID: doer.ID, + RepoName: repo.LowerName, + } + } + + // If from == rule name, admins are allowed to modify them. + if protectedBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, from); err != nil { + return "", err + } else if protectedBranch != nil && !perm.IsAdmin() { + return "", repo_model.ErrUserDoesNotHaveAccessToRepo{ + UserID: doer.ID, + RepoName: repo.LowerName, + } + } + if err := git_model.RenameBranch(ctx, repo, from, to, func(ctx context.Context, isDefault bool) error { err2 := gitRepo.RenameBranch(from, to) if err2 != nil { @@ -642,3 +666,72 @@ func SetRepoDefaultBranch(ctx context.Context, repo *repo_model.Repository, gitR return nil } + +// BranchDivergingInfo contains the information about the divergence of a head branch to the base branch. +type BranchDivergingInfo struct { + // whether the base branch contains new commits which are not in the head branch + BaseHasNewCommits bool + + // behind/after are number of commits that the head branch is behind/after the base branch, it's 0 if it's unable to calculate. + // there could be a case that BaseHasNewCommits=true while the behind/after are both 0 (unable to calculate). + HeadCommitsBehind int + HeadCommitsAhead int +} + +// GetBranchDivergingInfo returns the information about the divergence of a patch branch to the base branch. +func GetBranchDivergingInfo(ctx reqctx.RequestContext, baseRepo *repo_model.Repository, baseBranch string, headRepo *repo_model.Repository, headBranch string) (*BranchDivergingInfo, error) { + headGitBranch, err := git_model.GetBranch(ctx, headRepo.ID, headBranch) + if err != nil { + return nil, err + } + if headGitBranch.IsDeleted { + return nil, git_model.ErrBranchNotExist{ + BranchName: headBranch, + } + } + baseGitBranch, err := git_model.GetBranch(ctx, baseRepo.ID, baseBranch) + if err != nil { + return nil, err + } + if baseGitBranch.IsDeleted { + return nil, git_model.ErrBranchNotExist{ + BranchName: baseBranch, + } + } + + info := &BranchDivergingInfo{} + if headGitBranch.CommitID == baseGitBranch.CommitID { + return info, nil + } + + // if the fork repo has new commits, this call will fail because they are not in the base repo + // exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb + // so at the moment, we first check the update time, then check whether the fork branch has base's head + diff, err := git.GetDivergingCommits(ctx, baseRepo.RepoPath(), baseGitBranch.CommitID, headGitBranch.CommitID) + if err != nil { + info.BaseHasNewCommits = baseGitBranch.UpdatedUnix > headGitBranch.UpdatedUnix + if headRepo.IsFork && info.BaseHasNewCommits { + return info, nil + } + // if the base's update time is before the fork, check whether the base's head is in the fork + headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, headRepo) + if err != nil { + return nil, err + } + headCommit, err := headGitRepo.GetCommit(headGitBranch.CommitID) + if err != nil { + return nil, err + } + baseCommitID, err := git.NewIDFromString(baseGitBranch.CommitID) + if err != nil { + return nil, err + } + hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID) + info.BaseHasNewCommits = !hasPreviousCommit + return info, nil + } + + info.HeadCommitsBehind, info.HeadCommitsAhead = diff.Behind, diff.Ahead + info.BaseHasNewCommits = info.HeadCommitsBehind > 0 + return info, nil +} diff --git a/services/repository/merge_upstream.go b/services/repository/merge_upstream.go index ef161889c0..34e01df723 100644 --- a/services/repository/merge_upstream.go +++ b/services/repository/merge_upstream.go @@ -4,38 +4,38 @@ package repository import ( - "context" + "errors" "fmt" - git_model "code.gitea.io/gitea/models/git" issue_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/gitrepo" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/services/pull" ) -type UpstreamDivergingInfo struct { - BaseHasNewCommits bool - CommitsBehind int - CommitsAhead int -} - // MergeUpstream merges the base repository's default branch into the fork repository's current branch. -func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) { +func MergeUpstream(ctx reqctx.RequestContext, doer *user_model.User, repo *repo_model.Repository, branch string) (mergeStyle string, err error) { if err = repo.MustNotBeArchived(); err != nil { return "", err } if err = repo.GetBaseRepo(ctx); err != nil { return "", err } + divergingInfo, err := GetUpstreamDivergingInfo(ctx, repo, branch) + if err != nil { + return "", err + } + if !divergingInfo.BaseBranchHasNewCommits { + return "up-to-date", nil + } + err = git.Push(ctx, repo.BaseRepo.RepoPath(), git.PushOptions{ Remote: repo.RepoPath(), - Branch: fmt.Sprintf("%s:%s", repo.BaseRepo.DefaultBranch, branch), + Branch: fmt.Sprintf("%s:%s", divergingInfo.BaseBranchName, branch), Env: repo_module.PushingEnvironment(doer, repo), }) if err == nil { @@ -67,7 +67,7 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model. BaseRepoID: repo.BaseRepo.ID, BaseRepo: repo.BaseRepo, HeadBranch: branch, // maybe HeadCommitID is not needed - BaseBranch: repo.BaseRepo.DefaultBranch, + BaseBranch: divergingInfo.BaseBranchName, } fakeIssue.PullRequest = fakePR err = pull.Update(ctx, fakePR, doer, "merge upstream", false) @@ -77,68 +77,47 @@ func MergeUpstream(ctx context.Context, doer *user_model.User, repo *repo_model. return "merge", nil } +// UpstreamDivergingInfo is also used in templates, so it needs to search for all references before changing it. +type UpstreamDivergingInfo struct { + BaseBranchName string + BaseBranchHasNewCommits bool + HeadBranchCommitsBehind int +} + // GetUpstreamDivergingInfo returns the information about the divergence between the fork repository's branch and the base repository's default branch. -func GetUpstreamDivergingInfo(ctx reqctx.RequestContext, repo *repo_model.Repository, branch string) (*UpstreamDivergingInfo, error) { - if !repo.IsFork { +func GetUpstreamDivergingInfo(ctx reqctx.RequestContext, forkRepo *repo_model.Repository, forkBranch string) (*UpstreamDivergingInfo, error) { + if !forkRepo.IsFork { return nil, util.NewInvalidArgumentErrorf("repo is not a fork") } - if repo.IsArchived { + if forkRepo.IsArchived { return nil, util.NewInvalidArgumentErrorf("repo is archived") } - if err := repo.GetBaseRepo(ctx); err != nil { + if err := forkRepo.GetBaseRepo(ctx); err != nil { return nil, err } - forkBranch, err := git_model.GetBranch(ctx, repo.ID, branch) - if err != nil { - return nil, err + // Do the best to follow the GitHub's behavior, suppose there is a `branch-a` in fork repo: + // * if `branch-a` exists in base repo: try to sync `base:branch-a` to `fork:branch-a` + // * if `branch-a` doesn't exist in base repo: try to sync `base:main` to `fork:branch-a` + info, err := GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkBranch, forkRepo, forkBranch) + if err == nil { + return &UpstreamDivergingInfo{ + BaseBranchName: forkBranch, + BaseBranchHasNewCommits: info.BaseHasNewCommits, + HeadBranchCommitsBehind: info.HeadCommitsBehind, + }, nil } - - baseBranch, err := git_model.GetBranch(ctx, repo.BaseRepo.ID, repo.BaseRepo.DefaultBranch) - if err != nil { - return nil, err + if errors.Is(err, util.ErrNotExist) { + info, err = GetBranchDivergingInfo(ctx, forkRepo.BaseRepo, forkRepo.BaseRepo.DefaultBranch, forkRepo, forkBranch) + if err == nil { + return &UpstreamDivergingInfo{ + BaseBranchName: forkRepo.BaseRepo.DefaultBranch, + BaseBranchHasNewCommits: info.BaseHasNewCommits, + HeadBranchCommitsBehind: info.HeadCommitsBehind, + }, nil + } } - - info := &UpstreamDivergingInfo{} - if forkBranch.CommitID == baseBranch.CommitID { - return info, nil - } - - // if the fork repo has new commits, this call will fail because they are not in the base repo - // exit status 128 - fatal: Invalid symmetric difference expression aaaaaaaaaaaa...bbbbbbbbbbbb - // so at the moment, we first check the update time, then check whether the fork branch has base's head - diff, err := git.GetDivergingCommits(ctx, repo.BaseRepo.RepoPath(), baseBranch.CommitID, forkBranch.CommitID) - if err != nil { - info.BaseHasNewCommits = baseBranch.UpdatedUnix > forkBranch.UpdatedUnix - if info.BaseHasNewCommits { - return info, nil - } - - // if the base's update time is before the fork, check whether the base's head is in the fork - baseGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo.BaseRepo) - if err != nil { - return nil, err - } - headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, repo) - if err != nil { - return nil, err - } - - baseCommitID, err := baseGitRepo.ConvertToGitID(baseBranch.CommitID) - if err != nil { - return nil, err - } - headCommit, err := headGitRepo.GetCommit(forkBranch.CommitID) - if err != nil { - return nil, err - } - hasPreviousCommit, _ := headCommit.HasPreviousCommit(baseCommitID) - info.BaseHasNewCommits = !hasPreviousCommit - return info, nil - } - - info.CommitsBehind, info.CommitsAhead = diff.Behind, diff.Ahead - return info, nil + return nil, err } diff --git a/templates/admin/navbar.tmpl b/templates/admin/navbar.tmpl index 2d8d20691b..ce3048ed9f 100644 --- a/templates/admin/navbar.tmpl +++ b/templates/admin/navbar.tmpl @@ -98,7 +98,7 @@ <a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/-/admin/notices"> {{ctx.Locale.Tr "admin.notices"}} </a> - <details class="item toggleable-item" {{if or .PageIsAdminMonitorStats .PageIsAdminMonitorCron .PageIsAdminMonitorQueue .PageIsAdminMonitorStacktrace}}open{{end}}> + <details class="item toggleable-item" {{if or .PageIsAdminMonitorStats .PageIsAdminMonitorCron .PageIsAdminMonitorQueue .PageIsAdminMonitorTrace}}open{{end}}> <summary>{{ctx.Locale.Tr "admin.monitor"}}</summary> <div class="menu"> <a class="{{if .PageIsAdminMonitorStats}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stats"> @@ -110,8 +110,8 @@ <a class="{{if .PageIsAdminMonitorQueue}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/queue"> {{ctx.Locale.Tr "admin.monitor.queues"}} </a> - <a class="{{if .PageIsAdminMonitorStacktrace}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stacktrace"> - {{ctx.Locale.Tr "admin.monitor.stacktrace"}} + <a class="{{if .PageIsAdminMonitorTrace}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stacktrace"> + {{ctx.Locale.Tr "admin.monitor.trace"}} </a> </div> </details> diff --git a/templates/admin/perftrace.tmpl b/templates/admin/perftrace.tmpl new file mode 100644 index 0000000000..2e09f14e46 --- /dev/null +++ b/templates/admin/perftrace.tmpl @@ -0,0 +1,13 @@ +{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}} + +<div class="admin-setting-content"> + {{template "admin/trace_tabs" .}} + + {{range $record := .PerfTraceRecords}} + <div class="ui segment tw-w-full tw-overflow-auto"> + <pre class="tw-whitespace-pre">{{$record.Content}}</pre> + </div> + {{end}} +</div> + +{{template "admin/layout_footer" .}} diff --git a/templates/admin/stacktrace-row.tmpl b/templates/admin/stacktrace-row.tmpl index 97c361ff90..db7ed81c79 100644 --- a/templates/admin/stacktrace-row.tmpl +++ b/templates/admin/stacktrace-row.tmpl @@ -17,7 +17,10 @@ </div> <div> {{if or (eq .Process.Type "request") (eq .Process.Type "normal")}} - <a class="delete-button icon" href="" data-url="{{.root.Link}}/cancel/{{.Process.PID}}" data-id="{{.Process.PID}}" data-name="{{.Process.Description}}">{{svg "octicon-trash" 16 "text-red"}}</a> + <a class="link-action" data-url="{{.root.Link}}/cancel/{{.Process.PID}}" + data-modal-confirm-header="{{ctx.Locale.Tr "admin.monitor.process.cancel"}}" + data-modal-confirm-content="{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}" + >{{svg "octicon-trash" 16 "text-red"}}</a> {{end}} </div> </div> diff --git a/templates/admin/stacktrace.tmpl b/templates/admin/stacktrace.tmpl index ce03d80555..c5dde6b30c 100644 --- a/templates/admin/stacktrace.tmpl +++ b/templates/admin/stacktrace.tmpl @@ -1,22 +1,7 @@ {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}} <div class="admin-setting-content"> - <div class="tw-flex tw-items-center"> - <div class="tw-flex-1"> - <div class="ui compact small menu"> - <a class="{{if eq .ShowGoroutineList "process"}}active {{end}}item" href="?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a> - <a class="{{if eq .ShowGoroutineList "stacktrace"}}active {{end}}item" href="?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a> - </div> - </div> - <form target="_blank" action="{{AppSubUrl}}/-/admin/monitor/diagnosis" class="ui form"> - <div class="ui inline field"> - <button class="ui primary small button">{{ctx.Locale.Tr "admin.monitor.download_diagnosis_report"}}</button> - <input name="seconds" size="3" maxlength="3" value="10"> {{ctx.Locale.Tr "tool.raw_seconds"}} - </div> - </form> - </div> - - <div class="divider"></div> + {{template "admin/trace_tabs" .}} <h4 class="ui top attached header"> {{printf "%d Goroutines" .GoroutineCount}}{{/* Goroutine is non-translatable*/}} @@ -34,15 +19,4 @@ {{end}} </div> -<div class="ui g-modal-confirm delete modal"> - <div class="header"> - {{ctx.Locale.Tr "admin.monitor.process.cancel"}} - </div> - <div class="content"> - <p>{{ctx.Locale.Tr "admin.monitor.process.cancel_notices" (`<span class="name"></span>`|SafeHTML)}}</p> - <p>{{ctx.Locale.Tr "admin.monitor.process.cancel_desc"}}</p> - </div> - {{template "base/modal_actions_confirm" .}} -</div> - {{template "admin/layout_footer" .}} diff --git a/templates/admin/trace_tabs.tmpl b/templates/admin/trace_tabs.tmpl new file mode 100644 index 0000000000..5066c9c41b --- /dev/null +++ b/templates/admin/trace_tabs.tmpl @@ -0,0 +1,19 @@ +<div class="flex-text-block"> + <div class="tw-flex-1"> + <div class="ui compact small menu"> + {{if .ShowAdminPerformanceTraceTab}} + <a class="item {{Iif .PageIsAdminMonitorPerfTrace "active"}}" href="{{AppSubUrl}}/-/admin/monitor/perftrace">{{ctx.Locale.Tr "admin.monitor.performance_logs"}}</a> + {{end}} + <a class="item {{Iif (eq .ShowGoroutineList "process") "active"}}" href="{{AppSubUrl}}/-/admin/monitor/stacktrace?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a> + <a class="item {{Iif (eq .ShowGoroutineList "stacktrace") "active"}}" href="{{AppSubUrl}}/-/admin/monitor/stacktrace?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a> + </div> + </div> + <form target="_blank" action="{{AppSubUrl}}/-/admin/monitor/diagnosis" class="ui form"> + <div class="ui inline field"> + <button class="ui primary small button">{{ctx.Locale.Tr "admin.monitor.download_diagnosis_report"}}</button> + <input name="seconds" size="3" maxlength="3" value="10"> {{ctx.Locale.Tr "tool.raw_seconds"}} + </div> + </form> +</div> + +<div class="divider"></div> diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl index 04732d276a..a88ebec3bc 100644 --- a/templates/package/content/container.tmpl +++ b/templates/package/content/container.tmpl @@ -24,7 +24,7 @@ </div> </div> {{if .PackageDescriptor.Metadata.Manifests}} - <h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.multi_arch"}}</h4> + <h4 class="ui top attached header">{{ctx.Locale.Tr "packages.container.images"}}</h4> <div class="ui attached segment"> <table class="ui very basic compact table"> <thead> diff --git a/templates/package/content/maven.tmpl b/templates/package/content/maven.tmpl index f56595a830..e98fc53692 100644 --- a/templates/package/content/maven.tmpl +++ b/templates/package/content/maven.tmpl @@ -11,7 +11,7 @@ <div class="markup"><pre class="code-block"><code><repositories> <repository> <id>gitea</id> - <url><origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url></url> + <url><origin-url data-url="{{AppSubUrl}}/api/packages/{{.PackageDescriptor.Owner.Name}}/maven"></origin-url></url> </repository> </repositories> diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl index 25614d0df4..5d6a8ddf01 100644 --- a/templates/repo/branch/list.tmpl +++ b/templates/repo/branch/list.tmpl @@ -143,7 +143,7 @@ {{if .LatestPullRequest.HasMerged}} <a href="{{.LatestPullRequest.Issue.Link}}" class="ui purple large label">{{svg "octicon-git-merge" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.pulls.merged"}}</a> {{else if .LatestPullRequest.Issue.IsClosed}} - <a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a> + <a href="{{.LatestPullRequest.Issue.Link}}" class="ui red large label">{{svg "octicon-git-pull-request-closed" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.closed_title"}}</a> {{else}} <a href="{{.LatestPullRequest.Issue.Link}}" class="ui green large label">{{svg "octicon-git-pull-request" 16 "tw-mr-1"}}{{ctx.Locale.Tr "repo.issues.open_title"}}</a> {{end}} diff --git a/templates/repo/clone_panel.tmpl b/templates/repo/clone_panel.tmpl index c1c2c87b75..b813860150 100644 --- a/templates/repo/clone_panel.tmpl +++ b/templates/repo/clone_panel.tmpl @@ -1,5 +1,6 @@ <button class="ui primary button js-btn-clone-panel"> - <span>{{svg "octicon-code" 16}} Code</span> + {{svg "octicon-code" 16}} + <span>Code</span> {{svg "octicon-triangle-down" 14 "dropdown icon"}} </button> <div class="clone-panel-popup tippy-target"> diff --git a/templates/repo/code/upstream_diverging_info.tmpl b/templates/repo/code/upstream_diverging_info.tmpl index a1f37b8c05..b3d35c05e5 100644 --- a/templates/repo/code/upstream_diverging_info.tmpl +++ b/templates/repo/code/upstream_diverging_info.tmpl @@ -1,10 +1,12 @@ -{{if and .UpstreamDivergingInfo (or .UpstreamDivergingInfo.BaseHasNewCommits .UpstreamDivergingInfo.CommitsBehind)}} +{{if and .UpstreamDivergingInfo .UpstreamDivergingInfo.BaseBranchHasNewCommits}} <div class="ui message flex-text-block"> <div class="tw-flex-1"> - {{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.Repository.BaseRepo.DefaultBranch|PathEscapeSegments)}} - {{$upstreamHtml := HTMLFormat `<a href="%s">%s:%s</a>` $upstreamLink .Repository.BaseRepo.FullName .Repository.BaseRepo.DefaultBranch}} - {{if .UpstreamDivergingInfo.CommitsBehind}} - {{ctx.Locale.TrN .UpstreamDivergingInfo.CommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.CommitsBehind $upstreamHtml}} + {{$upstreamLink := printf "%s/src/branch/%s" .Repository.BaseRepo.Link (.UpstreamDivergingInfo.BaseBranchName|PathEscapeSegments)}} + {{$upstreamRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.BaseRepo.FullName .UpstreamDivergingInfo.BaseBranchName}} + {{$thisRepoBranchDisplay := HTMLFormat "%s:%s" .Repository.FullName .BranchName}} + {{$upstreamHtml := HTMLFormat `<a href="%s">%s</a>` $upstreamLink $upstreamRepoBranchDisplay}} + {{if .UpstreamDivergingInfo.HeadBranchCommitsBehind}} + {{ctx.Locale.TrN .UpstreamDivergingInfo.HeadBranchCommitsBehind "repo.pulls.upstream_diverging_prompt_behind_1" "repo.pulls.upstream_diverging_prompt_behind_n" .UpstreamDivergingInfo.HeadBranchCommitsBehind $upstreamHtml}} {{else}} {{ctx.Locale.Tr "repo.pulls.upstream_diverging_prompt_base_newer" $upstreamHtml}} {{end}} @@ -12,7 +14,7 @@ {{if .CanWriteCode}} <button class="ui compact primary button tw-m-0 link-action" data-modal-confirm-header="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}}" - data-modal-confirm-content="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge_confirm" .BranchName}}" + data-modal-confirm-content="{{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge_confirm" $upstreamRepoBranchDisplay $thisRepoBranchDisplay}}" data-url="{{.Repository.Link}}/branches/merge-upstream?branch={{.BranchName}}"> {{ctx.Locale.Tr "repo.pulls.upstream_diverging_merge"}} </button> diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl index 0354f6ef22..dcc1f48c2c 100644 --- a/templates/repo/issue/view_title.tmpl +++ b/templates/repo/issue/view_title.tmpl @@ -42,7 +42,7 @@ {{if .HasMerged}} <div class="ui purple label issue-state-label">{{svg "octicon-git-merge" 16 "tw-mr-1"}} {{if eq .Issue.PullRequest.Status 3}}{{ctx.Locale.Tr "repo.pulls.manually_merged"}}{{else}}{{ctx.Locale.Tr "repo.pulls.merged"}}{{end}}</div> {{else if .Issue.IsClosed}} - <div class="ui red label issue-state-label">{{svg (Iif .Issue.IsPull "octicon-git-pull-request" "octicon-issue-closed")}} {{ctx.Locale.Tr "repo.issues.closed_title"}}</div> + <div class="ui red label issue-state-label">{{svg (Iif .Issue.IsPull "octicon-git-pull-request-closed" "octicon-issue-closed")}} {{ctx.Locale.Tr "repo.issues.closed_title"}}</div> {{else if .Issue.IsPull}} {{if .IsPullWorkInProgress}} <div class="ui grey label issue-state-label">{{svg "octicon-git-pull-request-draft"}} {{ctx.Locale.Tr "repo.issues.draft_title"}}</div> diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl index 30244a8861..3533bfed0b 100644 --- a/templates/repo/sub_menu.tmpl +++ b/templates/repo/sub_menu.tmpl @@ -2,7 +2,7 @@ <div class="ui segments repository-summary tw-mt-1 tw-mb-0"> <div class="ui segment sub-menu repository-menu"> {{if and (.Permission.CanRead ctx.Consts.RepoUnitTypeCode) (not .IsEmptyRepo)}} - <a class="item muted {{if .PageIsCommits}}active{{end}}" href="{{.RepoLink}}/commits/{{.RefTypeNameSubURL}}"> + <a class="item muted {{if .PageIsCommits}}active{{end}}" href="{{.RepoLink}}/commits/{{.RefFullName.RefWebLinkPath}}"> {{svg "octicon-history"}} <b>{{ctx.Locale.PrettyNumber .CommitsCount}}</b> {{ctx.Locale.TrN .CommitsCount "repo.commit" "repo.commits"}} </a> <a class="item muted {{if .PageIsBranches}}active{{end}}" href="{{.RepoLink}}/branches"> diff --git a/templates/shared/issueicon.tmpl b/templates/shared/issueicon.tmpl index f828de5c66..bb6247c708 100644 --- a/templates/shared/issueicon.tmpl +++ b/templates/shared/issueicon.tmpl @@ -1,3 +1,4 @@ +{{/* the logic should be kept the same as getIssueIcon/getIssueColor in JS code */}} {{- if .IsPull -}} {{- if not .PullRequest -}} No PullRequest @@ -6,7 +7,7 @@ {{- if .PullRequest.HasMerged -}} {{- svg "octicon-git-merge" 16 "text purple" -}} {{- else -}} - {{- svg "octicon-git-pull-request" 16 "text red" -}} + {{- svg "octicon-git-pull-request-closed" 16 "text red" -}} {{- end -}} {{- else -}} {{- if .PullRequest.IsWorkInProgress ctx -}} diff --git a/templates/user/auth/link_account.tmpl b/templates/user/auth/link_account.tmpl index a99e172d05..d244ce38c2 100644 --- a/templates/user/auth/link_account.tmpl +++ b/templates/user/auth/link_account.tmpl @@ -16,13 +16,18 @@ </div> </overflow-menu> <div class="ui middle very relaxed page grid"> - <div class="column"> + <div class="column tw-my-5"> + {{/* these styles are quite tricky but it needs to be the same as the signin page */}} <div class="ui tab {{if not .user_exists}}active{{end}}" data-tab="auth-link-signup-tab"> + <div class="tw-flex tw-flex-col tw-gap-4 tw-max-w-2xl tw-m-auto"> {{if .AutoRegistrationFailedPrompt}}<div class="ui message">{{.AutoRegistrationFailedPrompt}}</div>{{end}} {{template "user/auth/signup_inner" .}} + </div> </div> <div class="ui tab {{if .user_exists}}active{{end}}" data-tab="auth-link-signin-tab"> + <div class="tw-flex tw-flex-col tw-gap-4 tw-max-w-2xl tw-m-auto"> {{template "user/auth/signin_inner" .}} + </div> </div> </div> </div> diff --git a/templates/user/auth/signin.tmpl b/templates/user/auth/signin.tmpl index 54cc82d49d..75e1bb27f9 100644 --- a/templates/user/auth/signin.tmpl +++ b/templates/user/auth/signin.tmpl @@ -1,6 +1,7 @@ {{template "base/head" .}} <div role="main" aria-label="{{.Title}}" class="page-content user signin{{if .LinkAccountMode}} icon{{end}}"> <div class="ui middle very relaxed page grid"> + {{/* these styles are quite tricky and should also apply to the signup and link_account pages */}} <div class="column tw-flex tw-flex-col tw-gap-4 tw-max-w-2xl tw-m-auto"> {{template "user/auth/signin_inner" .}} </div> diff --git a/templates/user/auth/signup_inner.tmpl b/templates/user/auth/signup_inner.tmpl index b3b2a4205e..d66568199d 100644 --- a/templates/user/auth/signup_inner.tmpl +++ b/templates/user/auth/signup_inner.tmpl @@ -59,12 +59,12 @@ </div> <div class="ui container fluid"> + {{if not .LinkAccountMode}} <div class="ui attached segment header top tw-flex tw-flex-col tw-items-center"> - {{if not .LinkAccountMode}} <div class="field"> <span>{{ctx.Locale.Tr "auth.already_have_account"}}</span> <a href="{{AppSubUrl}}/user/login">{{ctx.Locale.Tr "auth.sign_in_now"}}</a> </div> - {{end}} </div> + {{end}} </div> diff --git a/tests/e2e/utils_e2e.ts b/tests/e2e/utils_e2e.ts index 14ec836600..3e92e0d3c2 100644 --- a/tests/e2e/utils_e2e.ts +++ b/tests/e2e/utils_e2e.ts @@ -1,12 +1,13 @@ import {expect} from '@playwright/test'; import {env} from 'node:process'; +import type {Browser, Page, WorkerInfo} from '@playwright/test'; const ARTIFACTS_PATH = `tests/e2e/test-artifacts`; const LOGIN_PASSWORD = 'password'; // log in user and store session info. This should generally be // run in test.beforeAll(), then the session can be loaded in tests. -export async function login_user(browser, workerInfo, user) { +export async function login_user(browser: Browser, workerInfo: WorkerInfo, user: string) { // Set up a new context const context = await browser.newContext(); const page = await context.newPage(); @@ -17,8 +18,8 @@ export async function login_user(browser, workerInfo, user) { expect(response?.status()).toBe(200); // Status OK // Fill out form - await page.type('input[name=user_name]', user); - await page.type('input[name=password]', LOGIN_PASSWORD); + await page.locator('input[name=user_name]').fill(user); + await page.locator('input[name=password]').fill(LOGIN_PASSWORD); await page.click('form button.ui.primary.button:visible'); await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle @@ -31,7 +32,7 @@ export async function login_user(browser, workerInfo, user) { return context; } -export async function load_logged_in_context(browser, workerInfo, user) { +export async function load_logged_in_context(browser: Browser, workerInfo: WorkerInfo, user: string) { let context; try { context = await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`}); @@ -43,7 +44,7 @@ export async function load_logged_in_context(browser, workerInfo, user) { return context; } -export async function save_visual(page) { +export async function save_visual(page: Page) { // Optionally include visual testing if (env.VISUAL_TEST) { await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go index 8a0bd2e4ff..d64dd97f93 100644 --- a/tests/integration/api_branch_test.go +++ b/tests/integration/api_branch_test.go @@ -190,28 +190,61 @@ func testAPICreateBranch(t testing.TB, session *TestSession, user, repo, oldBran func TestAPIUpdateBranch(t *testing.T) { onGiteaRun(t, func(t *testing.T, _ *url.URL) { t.Run("UpdateBranchWithEmptyRepo", func(t *testing.T) { - testAPIUpdateBranch(t, "user10", "repo6", "master", "test", http.StatusNotFound) + testAPIUpdateBranch(t, "user10", "user10", "repo6", "master", "test", http.StatusNotFound) }) t.Run("UpdateBranchWithSameBranchNames", func(t *testing.T) { - resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "master", http.StatusUnprocessableEntity) + resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", "master", "master", http.StatusUnprocessableEntity) assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.") }) t.Run("UpdateBranchThatAlreadyExists", func(t *testing.T) { - resp := testAPIUpdateBranch(t, "user2", "repo1", "master", "branch2", http.StatusUnprocessableEntity) + resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", "master", "branch2", http.StatusUnprocessableEntity) assert.Contains(t, resp.Body.String(), "Cannot rename a branch using the same name or rename to a branch that already exists.") }) t.Run("UpdateBranchWithNonExistentBranch", func(t *testing.T) { - resp := testAPIUpdateBranch(t, "user2", "repo1", "i-dont-exist", "new-branch-name", http.StatusNotFound) + resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", "i-dont-exist", "new-branch-name", http.StatusNotFound) assert.Contains(t, resp.Body.String(), "Branch doesn't exist.") }) - t.Run("RenameBranchNormalScenario", func(t *testing.T) { - testAPIUpdateBranch(t, "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent) + t.Run("UpdateBranchWithNonAdminDoer", func(t *testing.T) { + // don't allow default branch renaming + resp := testAPIUpdateBranch(t, "user40", "user2", "repo1", "master", "new-branch-name", http.StatusForbidden) + assert.Contains(t, resp.Body.String(), "User must be a repo or site admin to rename default or protected branches.") + + // don't allow protected branch renaming + token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository) + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branches", &api.CreateBranchRepoOption{ + BranchName: "protected-branch", + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + testAPICreateBranchProtection(t, "protected-branch", 1, http.StatusCreated) + resp = testAPIUpdateBranch(t, "user40", "user2", "repo1", "protected-branch", "new-branch-name", http.StatusForbidden) + assert.Contains(t, resp.Body.String(), "User must be a repo or site admin to rename default or protected branches.") + }) + t.Run("UpdateBranchWithGlobedBasedProtectionRulesAndAdminAccess", func(t *testing.T) { + // don't allow branch that falls under glob-based protection rules to be renamed + token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteRepository) + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections", &api.BranchProtection{ + RuleName: "protected/**", + EnablePush: true, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + from := "protected/1" + req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branches", &api.CreateBranchRepoOption{ + BranchName: from, + }).AddTokenAuth(token) + MakeRequest(t, req, http.StatusCreated) + + resp := testAPIUpdateBranch(t, "user2", "user2", "repo1", from, "new-branch-name", http.StatusForbidden) + assert.Contains(t, resp.Body.String(), "Branch is protected by glob-based protection rules.") + }) + t.Run("UpdateBranchNormalScenario", func(t *testing.T) { + testAPIUpdateBranch(t, "user2", "user2", "repo1", "branch2", "new-branch-name", http.StatusNoContent) }) }) } -func testAPIUpdateBranch(t *testing.T, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder { - token := getUserToken(t, ownerName, auth_model.AccessTokenScopeWriteRepository) +func testAPIUpdateBranch(t *testing.T, doerName, ownerName, repoName, from, to string, expectedHTTPStatus int) *httptest.ResponseRecorder { + token := getUserToken(t, doerName, auth_model.AccessTokenScopeWriteRepository) req := NewRequestWithJSON(t, "PATCH", "api/v1/repos/"+ownerName+"/"+repoName+"/branches/"+from, &api.UpdateBranchRepoOption{ Name: to, }).AddTokenAuth(token) diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index 122afbfa08..13dc90f8a7 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -735,5 +735,5 @@ func TestAPIRepoGetAssignees(t *testing.T) { resp := MakeRequest(t, req, http.StatusOK) var assignees []*api.User DecodeJSON(t, resp, &assignees) - assert.Len(t, assignees, 1) + assert.Len(t, assignees, 2) } diff --git a/tests/integration/repo_branch_test.go b/tests/integration/repo_branch_test.go index 2b4c417334..f9cf13112a 100644 --- a/tests/integration/repo_branch_test.go +++ b/tests/integration/repo_branch_test.go @@ -11,13 +11,8 @@ import ( "strings" "testing" - auth_model "code.gitea.io/gitea/models/auth" - org_model "code.gitea.io/gitea/models/organization" - "code.gitea.io/gitea/models/perm" repo_model "code.gitea.io/gitea/models/repo" - "code.gitea.io/gitea/models/unit" "code.gitea.io/gitea/models/unittest" - api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/tests" @@ -142,19 +137,51 @@ func TestCreateBranchInvalidCSRF(t *testing.T) { assert.Contains(t, resp.Body.String(), "Invalid CSRF token") } -func prepareBranch(t *testing.T, session *TestSession, repo *repo_model.Repository) { - baseRefSubURL := fmt.Sprintf("branch/%s", repo.DefaultBranch) - +func prepareRecentlyPushedBranchTest(t *testing.T, headSession *TestSession, baseRepo, headRepo *repo_model.Repository) { + refSubURL := fmt.Sprintf("branch/%s", headRepo.DefaultBranch) + baseRepoPath := baseRepo.OwnerName + "/" + baseRepo.Name + headRepoPath := headRepo.OwnerName + "/" + headRepo.Name + // Case 1: Normal branch changeset to display pushed message // create branch with no new commit - testCreateBranch(t, session, repo.OwnerName, repo.Name, baseRefSubURL, "no-commit", http.StatusSeeOther) + testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, refSubURL, "no-commit", http.StatusSeeOther) // create branch with commit - testCreateBranch(t, session, repo.OwnerName, repo.Name, baseRefSubURL, "new-commit", http.StatusSeeOther) - testAPINewFile(t, session, repo.OwnerName, repo.Name, "new-commit", "new-commit.txt", "new-commit") + testAPINewFile(t, headSession, headRepo.OwnerName, headRepo.Name, "new-commit", fmt.Sprintf("new-file-%s.txt", headRepo.Name), "new-commit") - // create deleted branch - testCreateBranch(t, session, repo.OwnerName, repo.Name, "branch/new-commit", "deleted-branch", http.StatusSeeOther) - testUIDeleteBranch(t, session, repo.OwnerName, repo.Name, "deleted-branch") + // create a branch then delete it + testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "deleted-branch", http.StatusSeeOther) + testUIDeleteBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "deleted-branch") + + // only `new-commit` branch has commits ahead the base branch + checkRecentlyPushedNewBranches(t, headSession, headRepoPath, []string{"new-commit"}) + if baseRepo.RepoPath() != headRepo.RepoPath() { + checkRecentlyPushedNewBranches(t, headSession, baseRepoPath, []string{fmt.Sprintf("%v:new-commit", headRepo.FullName())}) + } + + // Case 2: Create PR so that `new-commit` branch will not show + testCreatePullToDefaultBranch(t, headSession, baseRepo, headRepo, "new-commit", "merge new-commit to default branch") + // No push message show because of active PR + checkRecentlyPushedNewBranches(t, headSession, headRepoPath, []string{}) + if baseRepo.RepoPath() != headRepo.RepoPath() { + checkRecentlyPushedNewBranches(t, headSession, baseRepoPath, []string{}) + } +} + +func prepareRecentlyPushedBranchSpecialTest(t *testing.T, session *TestSession, baseRepo, headRepo *repo_model.Repository) { + refSubURL := fmt.Sprintf("branch/%s", headRepo.DefaultBranch) + baseRepoPath := baseRepo.OwnerName + "/" + baseRepo.Name + headRepoPath := headRepo.OwnerName + "/" + headRepo.Name + // create branch with no new commit + testCreateBranch(t, session, headRepo.OwnerName, headRepo.Name, refSubURL, "no-commit-special", http.StatusSeeOther) + + // update base (default) branch before head branch is updated + testAPINewFile(t, session, baseRepo.OwnerName, baseRepo.Name, baseRepo.DefaultBranch, fmt.Sprintf("new-file-special-%s.txt", headRepo.Name), "new-commit") + + // Though we have new `no-commit` branch, but the headBranch is not newer or commits ahead baseBranch. No message show. + checkRecentlyPushedNewBranches(t, session, headRepoPath, []string{}) + if baseRepo.RepoPath() != headRepo.RepoPath() { + checkRecentlyPushedNewBranches(t, session, baseRepoPath, []string{}) + } } func testCreatePullToDefaultBranch(t *testing.T, session *TestSession, baseRepo, headRepo *repo_model.Repository, headBranch, title string) string { @@ -169,6 +196,9 @@ func testCreatePullToDefaultBranch(t *testing.T, session *TestSession, baseRepo, } func prepareRepoPR(t *testing.T, baseSession, headSession *TestSession, baseRepo, headRepo *repo_model.Repository) { + refSubURL := fmt.Sprintf("branch/%s", headRepo.DefaultBranch) + testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, refSubURL, "new-commit", http.StatusSeeOther) + // create opening PR testCreateBranch(t, headSession, headRepo.OwnerName, headRepo.Name, "branch/new-commit", "opening-pr", http.StatusSeeOther) testCreatePullToDefaultBranch(t, baseSession, baseRepo, headRepo, "opening-pr", "opening pr") @@ -210,65 +240,19 @@ func checkRecentlyPushedNewBranches(t *testing.T, session *TestSession, repoPath func TestRecentlyPushedNewBranches(t *testing.T) { onGiteaRun(t, func(t *testing.T, u *url.URL) { - user1Session := loginUser(t, "user1") - user2Session := loginUser(t, "user2") user12Session := loginUser(t, "user12") - user13Session := loginUser(t, "user13") - // prepare branch and PRs in original repo + // Same reposioty check repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10}) - prepareBranch(t, user12Session, repo10) prepareRepoPR(t, user12Session, user12Session, repo10, repo10) - - // outdated new branch should not be displayed - checkRecentlyPushedNewBranches(t, user12Session, "user12/repo10", []string{"new-commit"}) + prepareRecentlyPushedBranchTest(t, user12Session, repo10, repo10) + prepareRecentlyPushedBranchSpecialTest(t, user12Session, repo10, repo10) // create a fork repo in public org - testRepoFork(t, user12Session, repo10.OwnerName, repo10.Name, "org25", "org25_fork_repo10", "new-commit") + testRepoFork(t, user12Session, repo10.OwnerName, repo10.Name, "org25", "org25_fork_repo10", repo10.DefaultBranch) orgPublicForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 25, Name: "org25_fork_repo10"}) prepareRepoPR(t, user12Session, user12Session, repo10, orgPublicForkRepo) - - // user12 is the owner of the repo10 and the organization org25 - // in repo10, user12 has opening/closed/merged pr and closed/merged pr with deleted branch - checkRecentlyPushedNewBranches(t, user12Session, "user12/repo10", []string{"org25/org25_fork_repo10:new-commit", "new-commit"}) - - userForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11}) - testCtx := NewAPITestContext(t, repo10.OwnerName, repo10.Name, auth_model.AccessTokenScopeWriteRepository) - t.Run("AddUser13AsCollaborator", doAPIAddCollaborator(testCtx, "user13", perm.AccessModeWrite)) - prepareBranch(t, user13Session, userForkRepo) - prepareRepoPR(t, user13Session, user13Session, repo10, userForkRepo) - - // create branch with same name in different repo by user13 - testCreateBranch(t, user13Session, repo10.OwnerName, repo10.Name, "branch/new-commit", "same-name-branch", http.StatusSeeOther) - testCreateBranch(t, user13Session, userForkRepo.OwnerName, userForkRepo.Name, "branch/new-commit", "same-name-branch", http.StatusSeeOther) - testCreatePullToDefaultBranch(t, user13Session, repo10, userForkRepo, "same-name-branch", "same name branch pr") - - // user13 pushed 2 branches with the same name in repo10 and repo11 - // and repo11's branch has a pr, but repo10's branch doesn't - // in this case, we should get repo10's branch but not repo11's branch - checkRecentlyPushedNewBranches(t, user13Session, "user12/repo10", []string{"same-name-branch", "user13/repo11:new-commit"}) - - // create a fork repo in private org - testRepoFork(t, user1Session, repo10.OwnerName, repo10.Name, "private_org35", "org35_fork_repo10", "new-commit") - orgPrivateForkRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: 35, Name: "org35_fork_repo10"}) - prepareRepoPR(t, user1Session, user1Session, repo10, orgPrivateForkRepo) - - // user1 is the owner of private_org35 and no write permission to repo10 - // so user1 can only see the branch in org35_fork_repo10 - checkRecentlyPushedNewBranches(t, user1Session, "user12/repo10", []string{"private_org35/org35_fork_repo10:new-commit"}) - - // user2 push a branch in private_org35 - testCreateBranch(t, user2Session, orgPrivateForkRepo.OwnerName, orgPrivateForkRepo.Name, "branch/new-commit", "user-read-permission", http.StatusSeeOther) - // convert write permission to read permission for code unit - token := getTokenForLoggedInUser(t, user1Session, auth_model.AccessTokenScopeWriteOrganization) - req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/teams/%d", 24), &api.EditTeamOption{ - Name: "team24", - UnitsMap: map[string]string{"repo.code": "read"}, - }).AddTokenAuth(token) - MakeRequest(t, req, http.StatusOK) - teamUnit := unittest.AssertExistsAndLoadBean(t, &org_model.TeamUnit{TeamID: 24, Type: unit.TypeCode}) - assert.Equal(t, perm.AccessModeRead, teamUnit.AccessMode) - // user2 can see the branch as it is created by user2 - checkRecentlyPushedNewBranches(t, user2Session, "user12/repo10", []string{"private_org35/org35_fork_repo10:user-read-permission"}) + prepareRecentlyPushedBranchTest(t, user12Session, repo10, orgPublicForkRepo) + prepareRecentlyPushedBranchSpecialTest(t, user12Session, repo10, orgPublicForkRepo) }) } diff --git a/tests/integration/repo_merge_upstream_test.go b/tests/integration/repo_merge_upstream_test.go index e3e423c51d..e928b04e9b 100644 --- a/tests/integration/repo_merge_upstream_test.go +++ b/tests/integration/repo_merge_upstream_test.go @@ -60,25 +60,54 @@ func TestRepoMergeUpstream(t *testing.T) { t.Run("HeadBeforeBase", func(t *testing.T) { // add a file in base repo + sessionBaseUser := loginUser(t, baseUser.Name) require.NoError(t, createOrReplaceFileInBranch(baseUser, baseRepo, "new-file.txt", "master", "test-content-1")) - // the repo shows a prompt to "sync fork" var mergeUpstreamLink string - require.Eventually(t, func() bool { - resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK) - htmlDoc := NewHTMLParser(t, resp.Body) - mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc) - if mergeUpstreamLink == "" { - return false - } - respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html() - return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/master">user2/repo1:master</a>`) - }, 5*time.Second, 100*time.Millisecond) + t.Run("DetectDefaultBranch", func(t *testing.T) { + // the repo shows a prompt to "sync fork" (defaults to the default branch) + require.Eventually(t, func() bool { + resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc) + if mergeUpstreamLink == "" { + return false + } + respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html() + return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/master">user2/repo1:master</a>`) + }, 5*time.Second, 100*time.Millisecond) + }) + + t.Run("DetectSameBranch", func(t *testing.T) { + // if the fork-branch name also exists in the base repo, then use that branch instead + req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/_new/branch/master", map[string]string{ + "_csrf": GetUserCSRFToken(t, sessionBaseUser), + "new_branch_name": "fork-branch", + }) + sessionBaseUser.MakeRequest(t, req, http.StatusSeeOther) + + require.Eventually(t, func() bool { + resp := session.MakeRequest(t, NewRequestf(t, "GET", "/%s/test-repo-fork/src/branch/fork-branch", forkUser.Name), http.StatusOK) + htmlDoc := NewHTMLParser(t, resp.Body) + mergeUpstreamLink = queryMergeUpstreamButtonLink(htmlDoc) + if mergeUpstreamLink == "" { + return false + } + respMsg, _ := htmlDoc.Find(".ui.message:not(.positive)").Html() + return strings.Contains(respMsg, `This branch is 1 commit behind <a href="/user2/repo1/src/branch/fork-branch">user2/repo1:fork-branch</a>`) + }, 5*time.Second, 100*time.Millisecond) + }) // click the "sync fork" button req = NewRequestWithValues(t, "POST", mergeUpstreamLink, map[string]string{"_csrf": GetUserCSRFToken(t, session)}) session.MakeRequest(t, req, http.StatusOK) checkFileContent("fork-branch", "test-content-1") + + // delete the "fork-branch" from the base repo + req = NewRequestWithValues(t, "POST", "/user2/repo1/branches/delete?name=fork-branch", map[string]string{ + "_csrf": GetUserCSRFToken(t, sessionBaseUser), + }) + sessionBaseUser.MakeRequest(t, req, http.StatusOK) }) t.Run("BaseChangeAfterHeadChange", func(t *testing.T) { diff --git a/tests/integration/signin_test.go b/tests/integration/signin_test.go index d7c0b1bcd3..a76287ca94 100644 --- a/tests/integration/signin_test.go +++ b/tests/integration/signin_test.go @@ -15,8 +15,11 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/translation" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/tests" + "github.com/markbates/goth" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -98,6 +101,11 @@ func TestSigninWithRememberMe(t *testing.T) { func TestEnablePasswordSignInForm(t *testing.T) { defer tests.PrepareTestEnv(t)() + mockLinkAccount := func(ctx *context.Context) { + gothUser := goth.User{Email: "invalid-email", Name: "."} + _ = ctx.Session.Set("linkAccountGothUser", gothUser) + } + t.Run("EnablePasswordSignInForm=false", func(t *testing.T) { defer tests.PrintCurrentTest(t)() defer test.MockVariableValue(&setting.Service.EnablePasswordSignInForm, false)() @@ -108,6 +116,12 @@ func TestEnablePasswordSignInForm(t *testing.T) { req = NewRequest(t, "POST", "/user/login") MakeRequest(t, req, http.StatusForbidden) + + req = NewRequest(t, "GET", "/user/link_account") + defer web.RouteMockReset() + web.RouteMock(web.MockAfterMiddlewares, mockLinkAccount) + resp = MakeRequest(t, req, http.StatusOK) + NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", false) }) t.Run("EnablePasswordSignInForm=true", func(t *testing.T) { @@ -120,5 +134,11 @@ func TestEnablePasswordSignInForm(t *testing.T) { req = NewRequest(t, "POST", "/user/login") MakeRequest(t, req, http.StatusOK) + + req = NewRequest(t, "GET", "/user/link_account") + defer web.RouteMockReset() + web.RouteMock(web.MockAfterMiddlewares, mockLinkAccount) + resp = MakeRequest(t, req, http.StatusOK) + NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", true) }) } diff --git a/tsconfig.json b/tsconfig.json index d32cca0aaa..78b74a3d3c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,8 @@ "stripInternal": true, "strict": false, "strictFunctionTypes": true, + "noImplicitAny": true, + "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, "noPropertyAccessFromIndexSignature": false, diff --git a/updates.config.js b/updates.config.js index a4a2fa5228..4ef1ca701b 100644 --- a/updates.config.js +++ b/updates.config.js @@ -3,6 +3,7 @@ export default { '@mcaptcha/vanilla-glue', // breaking changes in rc versions need to be handled 'eslint', // need to migrate to eslint flat config first 'eslint-plugin-array-func', // need to migrate to eslint flat config first + 'eslint-plugin-github', // need to migrate to eslint 9 - https://github.com/github/eslint-plugin-github/issues/585 'eslint-plugin-no-use-extend-native', // need to migrate to eslint flat config first 'eslint-plugin-vitest', // need to migrate to eslint flat config first ], diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index 41793d60ed..876292fc94 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -1,5 +1,5 @@ <script lang="ts"> -import {createApp, nextTick} from 'vue'; +import {nextTick, defineComponent} from 'vue'; import {SvgIcon} from '../svg.ts'; import {GET} from '../modules/fetch.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; @@ -24,7 +24,7 @@ const commitStatus: CommitStatusMap = { warning: {name: 'gitea-exclamation', color: 'yellow'}, }; -const sfc = { +export default defineComponent({ components: {SvgIcon}, data() { const params = new URLSearchParams(window.location.search); @@ -130,12 +130,12 @@ const sfc = { }, methods: { - changeTab(t) { - this.tab = t; + changeTab(tab: string) { + this.tab = tab; this.updateHistory(); }, - changeReposFilter(filter) { + changeReposFilter(filter: string) { this.reposFilter = filter; this.repos = []; this.page = 1; @@ -218,7 +218,7 @@ const sfc = { this.searchRepos(); }, - changePage(page) { + changePage(page: number) { this.page = page; if (this.page > this.finalPage) { this.page = this.finalPage; @@ -256,7 +256,7 @@ const sfc = { } if (searchedURL === this.searchURL) { - this.repos = json.data.map((webSearchRepo) => { + this.repos = json.data.map((webSearchRepo: any) => { return { ...webSearchRepo.repository, latest_commit_status_state: webSearchRepo.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status @@ -264,7 +264,7 @@ const sfc = { locale_latest_commit_status_state: webSearchRepo.locale_latest_commit_status, }; }); - const count = response.headers.get('X-Total-Count'); + const count = Number(response.headers.get('X-Total-Count')); if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') { this.reposTotalCount = count; } @@ -275,7 +275,7 @@ const sfc = { } }, - repoIcon(repo) { + repoIcon(repo: any) { if (repo.fork) { return 'octicon-repo-forked'; } else if (repo.mirror) { @@ -298,7 +298,7 @@ const sfc = { return commitStatus[status].color; }, - reposFilterKeyControl(e) { + reposFilterKeyControl(e: KeyboardEvent) { switch (e.key) { case 'Enter': document.querySelector<HTMLAnchorElement>('.repo-owner-name-list li.active a')?.click(); @@ -335,16 +335,8 @@ const sfc = { } }, }, -}; +}); -export function initDashboardRepoList() { - const el = document.querySelector('#dashboard-repo-list'); - if (el) { - createApp(sfc).mount(el); - } -} - -export default sfc; // activate the IDE's Vue plugin </script> <template> <div> diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue index 3a394955ca..16760d1cb1 100644 --- a/web_src/js/components/DiffCommitSelector.vue +++ b/web_src/js/components/DiffCommitSelector.vue @@ -1,9 +1,26 @@ <script lang="ts"> +import {defineComponent} from 'vue'; import {SvgIcon} from '../svg.ts'; import {GET} from '../modules/fetch.ts'; import {generateAriaId} from '../modules/fomantic/base.ts'; -export default { +type Commit = { + id: string, + hovered: boolean, + selected: boolean, + summary: string, + committer_or_author_name: string, + time: string, + short_sha: string, +} + +type CommitListResult = { + commits: Array<Commit>, + last_review_commit_sha: string, + locale: Record<string, string>, +} + +export default defineComponent({ components: {SvgIcon}, data: () => { const el = document.querySelector('#diff-commit-select'); @@ -15,9 +32,9 @@ export default { locale: { filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'), } as Record<string, string>, - commits: [], + commits: [] as Array<Commit>, hoverActivated: false, - lastReviewCommitSha: null, + lastReviewCommitSha: '', uniqueIdMenu: generateAriaId(), uniqueIdShowAll: generateAriaId(), }; @@ -55,11 +72,11 @@ export default { switch (event.key) { case 'ArrowDown': // select next element event.preventDefault(); - this.focusElem(item.nextElementSibling, item); + this.focusElem(item.nextElementSibling as HTMLElement, item); break; case 'ArrowUp': // select previous element event.preventDefault(); - this.focusElem(item.previousElementSibling, item); + this.focusElem(item.previousElementSibling as HTMLElement, item); break; case 'Escape': // close menu event.preventDefault(); @@ -70,7 +87,7 @@ export default { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { const item = document.activeElement; // try to highlight the selected commits const commitIdx = item?.matches('.item') ? item.getAttribute('data-commit-idx') : null; - if (commitIdx) this.highlight(this.commits[commitIdx]); + if (commitIdx) this.highlight(this.commits[Number(commitIdx)]); } }, onKeyUp(event: KeyboardEvent) { @@ -86,7 +103,7 @@ export default { } } }, - highlight(commit) { + highlight(commit: Commit) { if (!this.hoverActivated) return; const indexSelected = this.commits.findIndex((x) => x.selected); const indexCurrentElem = this.commits.findIndex((x) => x.id === commit.id); @@ -118,16 +135,17 @@ export default { // set correct tabindex to allow easier navigation this.$nextTick(() => { if (this.menuVisible) { - this.focusElem(this.$refs.showAllChanges, this.$refs.expandBtn); + this.focusElem(this.$refs.showAllChanges as HTMLElement, this.$refs.expandBtn as HTMLElement); } else { - this.focusElem(this.$refs.expandBtn, this.$refs.showAllChanges); + this.focusElem(this.$refs.expandBtn as HTMLElement, this.$refs.showAllChanges as HTMLElement); } }); }, + /** Load the commits to show in this dropdown */ async fetchCommits() { const resp = await GET(`${this.issueLink}/commits/list`); - const results = await resp.json(); + const results = await resp.json() as CommitListResult; this.commits.push(...results.commits.map((x) => { x.hovered = false; return x; @@ -165,7 +183,7 @@ export default { * the diff from beginning of PR up to the second clicked commit is * opened */ - commitClickedShift(commit) { + commitClickedShift(commit: Commit) { this.hoverActivated = !this.hoverActivated; commit.selected = true; // Second click -> determine our range and open links accordingly @@ -188,7 +206,7 @@ export default { } }, }, -}; +}); </script> <template> <div class="ui scrolling dropdown custom diff-commit-selector"> diff --git a/web_src/js/components/DiffFileList.vue b/web_src/js/components/DiffFileList.vue index 2888c53d2e..6570c92781 100644 --- a/web_src/js/components/DiffFileList.vue +++ b/web_src/js/components/DiffFileList.vue @@ -17,18 +17,18 @@ function toggleFileList() { store.fileListIsVisible = !store.fileListIsVisible; } -function diffTypeToString(pType) { - const diffTypes = { - 1: 'add', - 2: 'modify', - 3: 'del', - 4: 'rename', - 5: 'copy', +function diffTypeToString(pType: number) { + const diffTypes: Record<string, string> = { + '1': 'add', + '2': 'modify', + '3': 'del', + '4': 'rename', + '5': 'copy', }; - return diffTypes[pType]; + return diffTypes[String(pType)]; } -function diffStatsWidth(adds, dels) { +function diffStatsWidth(adds: number, dels: number) { return `${adds / (adds + dels) * 100}%`; } diff --git a/web_src/js/components/DiffFileTree.vue b/web_src/js/components/DiffFileTree.vue index 9eabc65ae9..d00d03565f 100644 --- a/web_src/js/components/DiffFileTree.vue +++ b/web_src/js/components/DiffFileTree.vue @@ -1,5 +1,5 @@ <script lang="ts" setup> -import DiffFileTreeItem from './DiffFileTreeItem.vue'; +import DiffFileTreeItem, {type Item} from './DiffFileTreeItem.vue'; import {loadMoreFiles} from '../features/repo-diff.ts'; import {toggleElem} from '../utils/dom.ts'; import {diffTreeStore} from '../modules/stores.ts'; @@ -11,7 +11,7 @@ const LOCAL_STORAGE_KEY = 'diff_file_tree_visible'; const store = diffTreeStore(); const fileTree = computed(() => { - const result = []; + const result: Array<Item> = []; for (const file of store.files) { // Split file into directories const splits = file.Name.split('/'); @@ -24,15 +24,10 @@ const fileTree = computed(() => { if (index === splits.length) { isFile = true; } - let newParent = { + let newParent: Item = { name: split, children: [], isFile, - } as { - name: string, - children: any[], - isFile: boolean, - file?: any, }; if (isFile === true) { @@ -60,7 +55,7 @@ const fileTree = computed(() => { parent = newParent; } } - const mergeChildIfOnlyOneDir = (entries) => { + const mergeChildIfOnlyOneDir = (entries: Array<Record<string, any>>) => { for (const entry of entries) { if (entry.children) { mergeChildIfOnlyOneDir(entry.children); @@ -110,13 +105,13 @@ function toggleVisibility() { updateVisibility(!store.fileTreeIsVisible); } -function updateVisibility(visible) { +function updateVisibility(visible: boolean) { store.fileTreeIsVisible = visible; localStorage.setItem(LOCAL_STORAGE_KEY, store.fileTreeIsVisible); updateState(store.fileTreeIsVisible); } -function updateState(visible) { +function updateState(visible: boolean) { const btn = document.querySelector('.diff-toggle-file-tree-button'); const [toShow, toHide] = btn.querySelectorAll('.icon'); const tree = document.querySelector('#diff-file-tree'); diff --git a/web_src/js/components/DiffFileTreeItem.vue b/web_src/js/components/DiffFileTreeItem.vue index 31ce94aacd..d3be10e3e9 100644 --- a/web_src/js/components/DiffFileTreeItem.vue +++ b/web_src/js/components/DiffFileTreeItem.vue @@ -1,5 +1,5 @@ <script lang="ts" setup> -import {SvgIcon} from '../svg.ts'; +import {SvgIcon, type SvgName} from '../svg.ts'; import {diffTreeStore} from '../modules/stores.ts'; import {ref} from 'vue'; @@ -11,7 +11,7 @@ type File = { IsSubmodule: boolean; } -type Item = { +export type Item = { name: string; isFile: boolean; file?: File; @@ -25,18 +25,18 @@ defineProps<{ const store = diffTreeStore(); const collapsed = ref(false); -function getIconForDiffType(pType) { - const diffTypes = { - 1: {name: 'octicon-diff-added', classes: ['text', 'green']}, - 2: {name: 'octicon-diff-modified', classes: ['text', 'yellow']}, - 3: {name: 'octicon-diff-removed', classes: ['text', 'red']}, - 4: {name: 'octicon-diff-renamed', classes: ['text', 'teal']}, - 5: {name: 'octicon-diff-renamed', classes: ['text', 'green']}, // there is no octicon for copied, so renamed should be ok +function getIconForDiffType(pType: number) { + const diffTypes: Record<string, {name: SvgName, classes: Array<string>}> = { + '1': {name: 'octicon-diff-added', classes: ['text', 'green']}, + '2': {name: 'octicon-diff-modified', classes: ['text', 'yellow']}, + '3': {name: 'octicon-diff-removed', classes: ['text', 'red']}, + '4': {name: 'octicon-diff-renamed', classes: ['text', 'teal']}, + '5': {name: 'octicon-diff-renamed', classes: ['text', 'green']}, // there is no octicon for copied, so renamed should be ok }; - return diffTypes[pType]; + return diffTypes[String(pType)]; } -function fileIcon(file) { +function fileIcon(file: File) { if (file.IsSubmodule) { return 'octicon-file-submodule'; } diff --git a/web_src/js/components/PullRequestMergeForm.vue b/web_src/js/components/PullRequestMergeForm.vue index 3be7b802a3..4f291f5ca1 100644 --- a/web_src/js/components/PullRequestMergeForm.vue +++ b/web_src/js/components/PullRequestMergeForm.vue @@ -36,17 +36,17 @@ const forceMerge = computed(() => { }); watch(mergeStyle, (val) => { - mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e) => e.name === val); + mergeStyleDetail.value = mergeForm.value.mergeStyles.find((e: any) => e.name === val); for (const elem of document.querySelectorAll('[data-pull-merge-style]')) { toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val); } }); onMounted(() => { - mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0); + mergeStyleAllowedCount.value = mergeForm.value.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0); - let mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name; - if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed)?.name; + let mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name; + if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed)?.name; switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow); document.addEventListener('mouseup', hideMergeStyleMenu); @@ -68,7 +68,7 @@ function toggleActionForm(show: boolean) { mergeMessageFieldValue.value = mergeStyleDetail.value.mergeMessageFieldText; } -function switchMergeStyle(name, autoMerge = false) { +function switchMergeStyle(name: string, autoMerge = false) { mergeStyle.value = name; autoMergeWhenSucceed.value = autoMerge; } diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 914c9e76de..03c8464060 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -1,11 +1,12 @@ <script lang="ts"> import {SvgIcon} from '../svg.ts'; import ActionRunStatus from './ActionRunStatus.vue'; -import {createApp} from 'vue'; +import {defineComponent, type PropType} from 'vue'; import {createElementFromAttrs, toggleElem} from '../utils/dom.ts'; import {formatDatetime} from '../utils/time.ts'; import {renderAnsi} from '../render/ansi.ts'; import {POST, DELETE} from '../modules/fetch.ts'; +import type {IntervalId} from '../types.ts'; // see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts" type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked'; @@ -24,6 +25,20 @@ type LogLineCommand = { prefix: string, } +type Job = { + id: number; + name: string; + status: RunStatus; + canRerun: boolean; + duration: string; +} + +type Step = { + summary: string, + duration: string, + status: RunStatus, +} + function parseLineCommand(line: LogLine): LogLineCommand | null { for (const prefix of LogLinePrefixesGroup) { if (line.message.startsWith(prefix)) { @@ -38,7 +53,7 @@ function parseLineCommand(line: LogLine): LogLineCommand | null { return null; } -function isLogElementInViewport(el: HTMLElement): boolean { +function isLogElementInViewport(el: Element): boolean { const rect = el.getBoundingClientRect(); return rect.top >= 0 && rect.bottom <= window.innerHeight; // only check height but not width } @@ -57,25 +72,28 @@ function getLocaleStorageOptions(): LocaleStorageOptions { return {autoScroll: true, expandRunning: false}; } -const sfc = { +export default defineComponent({ name: 'RepoActionView', components: { SvgIcon, ActionRunStatus, }, props: { - runIndex: String, - jobIndex: String, - actionsURL: String, - locale: Object, - }, - - watch: { - optionAlwaysAutoScroll() { - this.saveLocaleStorageOptions(); + runIndex: { + type: String, + default: '', }, - optionAlwaysExpandRunning() { - this.saveLocaleStorageOptions(); + jobIndex: { + type: String, + default: '', + }, + actionsURL: { + type: String, + default: '', + }, + locale: { + type: Object as PropType<Record<string, any>>, + default: null, }, }, @@ -83,10 +101,10 @@ const sfc = { const {autoScroll, expandRunning} = getLocaleStorageOptions(); return { // internal state - loadingAbortController: null, - intervalID: null, - currentJobStepsStates: [], - artifacts: [], + loadingAbortController: null as AbortController | null, + intervalID: null as IntervalId | null, + currentJobStepsStates: [] as Array<Record<string, any>>, + artifacts: [] as Array<Record<string, any>>, onHoverRerunIndex: -1, menuVisible: false, isFullScreen: false, @@ -102,10 +120,11 @@ const sfc = { link: '', title: '', titleHTML: '', - status: '', + status: 'unknown' as RunStatus, canCancel: false, canApprove: false, canRerun: false, + canDeleteArtifact: false, done: false, workflowID: '', workflowLink: '', @@ -118,7 +137,7 @@ const sfc = { // canRerun: false, // duration: '', // }, - ], + ] as Array<Job>, commit: { localeCommit: '', localePushedBy: '', @@ -131,6 +150,7 @@ const sfc = { branch: { name: '', link: '', + isDeleted: false, }, }, }, @@ -143,12 +163,21 @@ const sfc = { // duration: '', // status: '', // } - ], + ] as Array<Step>, }, }; }, - async mounted() { + watch: { + optionAlwaysAutoScroll() { + this.saveLocaleStorageOptions(); + }, + optionAlwaysExpandRunning() { + this.saveLocaleStorageOptions(); + }, + }, + + async mounted() { // eslint-disable-line @typescript-eslint/no-misused-promises // load job data and then auto-reload periodically // need to await first loadJob so this.currentJobStepsStates is initialized and can be used in hashChangeListener await this.loadJob(); @@ -180,17 +209,18 @@ const sfc = { // get the job step logs container ('.job-step-logs') getJobStepLogsContainer(stepIndex: number): HTMLElement { - return this.$refs.logs[stepIndex]; + return (this.$refs.logs as any)[stepIndex]; }, // get the active logs container element, either the `job-step-logs` or the `job-log-list` in the `job-log-group` getActiveLogsContainer(stepIndex: number): HTMLElement { const el = this.getJobStepLogsContainer(stepIndex); + // @ts-expect-error - _stepLogsActiveContainer is a custom property return el._stepLogsActiveContainer ?? el; }, // begin a log group beginLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) { - const el = this.$refs.logs[stepIndex]; + const el = (this.$refs.logs as any)[stepIndex]; const elJobLogGroupSummary = createElementFromAttrs('summary', {class: 'job-log-group-summary'}, this.createLogLine(stepIndex, startTime, { index: line.index, @@ -208,7 +238,7 @@ const sfc = { }, // end a log group endLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) { - const el = this.$refs.logs[stepIndex]; + const el = (this.$refs.logs as any)[stepIndex]; el._stepLogsActiveContainer = null; el.append(this.createLogLine(stepIndex, startTime, { index: line.index, @@ -263,7 +293,7 @@ const sfc = { const el = this.getJobStepLogsContainer(stepIndex); // if the logs container is empty, then auto-scroll if the step is expanded if (!el.lastChild) return this.currentJobStepsStates[stepIndex].expanded; - return isLogElementInViewport(el.lastChild); + return isLogElementInViewport(el.lastChild as Element); }, appendLogs(stepIndex: number, startTime: number, logLines: LogLine[]) { @@ -378,9 +408,9 @@ const sfc = { if (this.menuVisible) this.menuVisible = false; }, - toggleTimeDisplay(type: string) { + toggleTimeDisplay(type: 'seconds' | 'stamp') { this.timeVisible[`log-time-${type}`] = !this.timeVisible[`log-time-${type}`]; - for (const el of this.$refs.steps.querySelectorAll(`.log-time-${type}`)) { + for (const el of (this.$refs.steps as HTMLElement).querySelectorAll(`.log-time-${type}`)) { toggleElem(el, this.timeVisible[`log-time-${type}`]); } }, @@ -407,66 +437,20 @@ const sfc = { const selectedLogStep = window.location.hash; if (!selectedLogStep) return; const [_, step, _line] = selectedLogStep.split('-'); - if (!this.currentJobStepsStates[step]) return; - if (!this.currentJobStepsStates[step].expanded && this.currentJobStepsStates[step].cursor === null) { - this.currentJobStepsStates[step].expanded = true; + const stepNum = Number(step); + if (!this.currentJobStepsStates[stepNum]) return; + if (!this.currentJobStepsStates[stepNum].expanded && this.currentJobStepsStates[stepNum].cursor === null) { + this.currentJobStepsStates[stepNum].expanded = true; // need to await for load job if the step log is loaded for the first time // so logline can be selected by querySelector await this.loadJob(); } - const logLine = this.$refs.steps.querySelector(selectedLogStep); + const logLine = (this.$refs.steps as HTMLElement).querySelector(selectedLogStep); if (!logLine) return; - logLine.querySelector('.line-num').click(); + logLine.querySelector<HTMLAnchorElement>('.line-num').click(); }, }, -}; - -export default sfc; - -export function initRepositoryActionView() { - const el = document.querySelector('#repo-action-view'); - if (!el) return; - - // TODO: the parent element's full height doesn't work well now, - // but we can not pollute the global style at the moment, only fix the height problem for pages with this component - const parentFullHeight = document.querySelector<HTMLElement>('body > div.full.height'); - if (parentFullHeight) parentFullHeight.style.paddingBottom = '0'; - - const view = createApp(sfc, { - runIndex: el.getAttribute('data-run-index'), - jobIndex: el.getAttribute('data-job-index'), - actionsURL: el.getAttribute('data-actions-url'), - locale: { - approve: el.getAttribute('data-locale-approve'), - cancel: el.getAttribute('data-locale-cancel'), - rerun: el.getAttribute('data-locale-rerun'), - rerun_all: el.getAttribute('data-locale-rerun-all'), - scheduled: el.getAttribute('data-locale-runs-scheduled'), - commit: el.getAttribute('data-locale-runs-commit'), - pushedBy: el.getAttribute('data-locale-runs-pushed-by'), - artifactsTitle: el.getAttribute('data-locale-artifacts-title'), - areYouSure: el.getAttribute('data-locale-are-you-sure'), - confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'), - showTimeStamps: el.getAttribute('data-locale-show-timestamps'), - showLogSeconds: el.getAttribute('data-locale-show-log-seconds'), - showFullScreen: el.getAttribute('data-locale-show-full-screen'), - downloadLogs: el.getAttribute('data-locale-download-logs'), - status: { - unknown: el.getAttribute('data-locale-status-unknown'), - waiting: el.getAttribute('data-locale-status-waiting'), - running: el.getAttribute('data-locale-status-running'), - success: el.getAttribute('data-locale-status-success'), - failure: el.getAttribute('data-locale-status-failure'), - cancelled: el.getAttribute('data-locale-status-cancelled'), - skipped: el.getAttribute('data-locale-status-skipped'), - blocked: el.getAttribute('data-locale-status-blocked'), - }, - logsAlwaysAutoScroll: el.getAttribute('data-locale-logs-always-auto-scroll'), - logsAlwaysExpandRunning: el.getAttribute('data-locale-logs-always-expand-running'), - }, - }); - view.mount(el); -} +}); </script> <template> <div class="ui container action-view-container"> diff --git a/web_src/js/components/RepoActivityTopAuthors.vue b/web_src/js/components/RepoActivityTopAuthors.vue index 1f5e9825ba..77b85bd7e2 100644 --- a/web_src/js/components/RepoActivityTopAuthors.vue +++ b/web_src/js/components/RepoActivityTopAuthors.vue @@ -1,4 +1,5 @@ <script lang="ts" setup> +// @ts-expect-error - module exports no types import {VueBarGraph} from 'vue-bar-graph'; import {computed, onMounted, ref} from 'vue'; @@ -8,13 +9,15 @@ const colors = ref({ textAltColor: 'white', }); -// possible keys: -// * avatar_link: (...) -// * commits: (...) -// * home_link: (...) -// * login: (...) -// * name: (...) -const activityTopAuthors = window.config.pageData.repoActivityTopAuthors || []; +type ActivityAuthorData = { + avatar_link: string; + commits: number; + home_link: string; + login: string; + name: string; +} + +const activityTopAuthors: Array<ActivityAuthorData> = window.config.pageData.repoActivityTopAuthors || []; const graphPoints = computed(() => { return activityTopAuthors.map((item) => { @@ -26,7 +29,7 @@ const graphPoints = computed(() => { }); const graphAuthors = computed(() => { - return activityTopAuthors.map((item, idx) => { + return activityTopAuthors.map((item, idx: number) => { return { position: idx + 1, ...item, diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue index a5ed8b6dad..fa5c75af99 100644 --- a/web_src/js/components/RepoBranchTagSelector.vue +++ b/web_src/js/components/RepoBranchTagSelector.vue @@ -1,5 +1,5 @@ <script lang="ts"> -import {nextTick} from 'vue'; +import {defineComponent, nextTick} from 'vue'; import {SvgIcon} from '../svg.ts'; import {showErrorToast} from '../modules/toast.ts'; import {GET} from '../modules/fetch.ts'; @@ -17,51 +17,11 @@ type SelectedTab = 'branches' | 'tags'; type TabLoadingStates = Record<SelectedTab, '' | 'loading' | 'done'> -const sfc = { +export default defineComponent({ components: {SvgIcon}, props: { elRoot: HTMLElement, }, - computed: { - searchFieldPlaceholder() { - return this.selectedTab === 'branches' ? this.textFilterBranch : this.textFilterTag; - }, - filteredItems(): ListItem[] { - const searchTermLower = this.searchTerm.toLowerCase(); - const items = this.allItems.filter((item: ListItem) => { - const typeMatched = (this.selectedTab === 'branches' && item.refType === 'branch') || (this.selectedTab === 'tags' && item.refType === 'tag'); - if (!typeMatched) return false; - if (!this.searchTerm) return true; // match all - return item.refShortName.toLowerCase().includes(searchTermLower); - }); - - // TODO: fix this anti-pattern: side-effects-in-computed-properties - this.activeItemIndex = !items.length && this.showCreateNewRef ? 0 : -1; - return items; - }, - showNoResults() { - if (this.tabLoadingStates[this.selectedTab] !== 'done') return false; - return !this.filteredItems.length && !this.showCreateNewRef; - }, - showCreateNewRef() { - if (!this.allowCreateNewRef || !this.searchTerm) { - return false; - } - return !this.allItems.filter((item: ListItem) => { - return item.refShortName === this.searchTerm; // FIXME: not quite right here, it mixes "branch" and "tag" names - }).length; - }, - createNewRefFormActionUrl() { - return `${this.currentRepoLink}/branches/_new/${this.currentRefType}/${pathEscapeSegments(this.currentRefShortName)}`; - }, - }, - watch: { - menuVisible(visible: boolean) { - if (!visible) return; - this.focusSearchField(); - this.loadTabItems(); - }, - }, data() { const shouldShowTabBranches = this.elRoot.getAttribute('data-show-tab-branches') === 'true'; return { @@ -89,7 +49,7 @@ const sfc = { currentRepoDefaultBranch: this.elRoot.getAttribute('data-current-repo-default-branch'), currentRepoLink: this.elRoot.getAttribute('data-current-repo-link'), currentTreePath: this.elRoot.getAttribute('data-current-tree-path'), - currentRefType: this.elRoot.getAttribute('data-current-ref-type'), + currentRefType: this.elRoot.getAttribute('data-current-ref-type') as GitRefType, currentRefShortName: this.elRoot.getAttribute('data-current-ref-short-name'), refLinkTemplate: this.elRoot.getAttribute('data-ref-link-template'), @@ -102,6 +62,46 @@ const sfc = { enableFeed: this.elRoot.getAttribute('data-enable-feed') === 'true', }; }, + computed: { + searchFieldPlaceholder() { + return this.selectedTab === 'branches' ? this.textFilterBranch : this.textFilterTag; + }, + filteredItems(): ListItem[] { + const searchTermLower = this.searchTerm.toLowerCase(); + const items = this.allItems.filter((item: ListItem) => { + const typeMatched = (this.selectedTab === 'branches' && item.refType === 'branch') || (this.selectedTab === 'tags' && item.refType === 'tag'); + if (!typeMatched) return false; + if (!this.searchTerm) return true; // match all + return item.refShortName.toLowerCase().includes(searchTermLower); + }); + + // TODO: fix this anti-pattern: side-effects-in-computed-properties + this.activeItemIndex = !items.length && this.showCreateNewRef ? 0 : -1; // eslint-disable-line vue/no-side-effects-in-computed-properties + return items; + }, + showNoResults() { + if (this.tabLoadingStates[this.selectedTab] !== 'done') return false; + return !this.filteredItems.length && !this.showCreateNewRef; + }, + showCreateNewRef() { + if (!this.allowCreateNewRef || !this.searchTerm) { + return false; + } + return !this.allItems.filter((item: ListItem) => { + return item.refShortName === this.searchTerm; // FIXME: not quite right here, it mixes "branch" and "tag" names + }).length; + }, + createNewRefFormActionUrl() { + return `${this.currentRepoLink}/branches/_new/${this.currentRefType}/${pathEscapeSegments(this.currentRefShortName)}`; + }, + }, + watch: { + menuVisible(visible: boolean) { + if (!visible) return; + this.focusSearchField(); + this.loadTabItems(); + }, + }, beforeMount() { document.body.addEventListener('click', (e) => { if (this.$el.contains(e.target)) return; @@ -139,11 +139,11 @@ const sfc = { } }, createNewRef() { - this.$refs.createNewRefForm?.submit(); + (this.$refs.createNewRefForm as HTMLFormElement)?.submit(); }, focusSearchField() { nextTick(() => { - this.$refs.searchField.focus(); + (this.$refs.searchField as HTMLInputElement).focus(); }); }, getSelectedIndexInFiltered() { @@ -154,9 +154,10 @@ const sfc = { }, getActiveItem() { const el = this.$refs[`listItem${this.activeItemIndex}`]; // eslint-disable-line no-jquery/variable-pattern + // @ts-expect-error - el is unknown type return (el && el.length) ? el[0] : null; }, - keydown(e) { + keydown(e: KeyboardEvent) { if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { e.preventDefault(); @@ -180,7 +181,7 @@ const sfc = { this.menuVisible = false; } }, - handleTabSwitch(selectedTab) { + handleTabSwitch(selectedTab: SelectedTab) { this.selectedTab = selectedTab; this.focusSearchField(); this.loadTabItems(); @@ -212,9 +213,7 @@ const sfc = { } }, }, -}; - -export default sfc; // activate IDE's Vue plugin +}); </script> <template> <div class="ui dropdown custom branch-selector-dropdown ellipsis-items-nowrap"> diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue index e79fc80d8e..6ad2c848b1 100644 --- a/web_src/js/components/RepoContributors.vue +++ b/web_src/js/components/RepoContributors.vue @@ -1,4 +1,5 @@ <script lang="ts"> +import {defineComponent, type PropType} from 'vue'; import {SvgIcon} from '../svg.ts'; import dayjs from 'dayjs'; import { @@ -56,11 +57,11 @@ Chart.register( customEventListener, ); -export default { +export default defineComponent({ components: {ChartLine, SvgIcon}, props: { locale: { - type: Object, + type: Object as PropType<Record<string, any>>, required: true, }, repoLink: { @@ -79,16 +80,16 @@ export default { sortedContributors: {} as Record<string, any>, type: 'commits', contributorsStats: {} as Record<string, any>, - xAxisStart: null, - xAxisEnd: null, - xAxisMin: null, - xAxisMax: null, + xAxisStart: null as number | null, + xAxisEnd: null as number | null, + xAxisMin: null as number | null, + xAxisMax: null as number | null, }), mounted() { this.fetchGraphData(); fomanticQuery('#repo-contributors').dropdown({ - onChange: (val) => { + onChange: (val: string) => { this.xAxisMin = this.xAxisStart; this.xAxisMax = this.xAxisEnd; this.type = val; @@ -98,7 +99,7 @@ export default { }, methods: { sortContributors() { - const contributors = this.filterContributorWeeksByDateRange(); + const contributors: Record<string, any> = this.filterContributorWeeksByDateRange(); const criteria = `total_${this.type}`; this.sortedContributors = Object.values(contributors) .filter((contributor) => contributor[criteria] !== 0) @@ -157,7 +158,7 @@ export default { }, filterContributorWeeksByDateRange() { - const filteredData = {}; + const filteredData: Record<string, any> = {}; const data = this.contributorsStats; for (const key of Object.keys(data)) { const user = data[key]; @@ -195,7 +196,7 @@ export default { // Normally, chartjs handles this automatically, but it will resize the graph when you // zoom, pan etc. I think resizing the graph makes it harder to compare things visually. const maxValue = Math.max( - ...this.totalStats.weeks.map((o) => o[this.type]), + ...this.totalStats.weeks.map((o: Record<string, any>) => o[this.type]), ); const [coefficient, exp] = maxValue.toExponential().split('e').map(Number); if (coefficient % 1 === 0) return maxValue; @@ -207,7 +208,7 @@ export default { // for contributors' graph. If I let chartjs do this for me, it will choose different // maxY value for each contributors' graph which again makes it harder to compare. const maxValue = Math.max( - ...this.sortedContributors.map((c) => c.max_contribution_type), + ...this.sortedContributors.map((c: Record<string, any>) => c.max_contribution_type), ); const [coefficient, exp] = maxValue.toExponential().split('e').map(Number); if (coefficient % 1 === 0) return maxValue; @@ -231,8 +232,8 @@ export default { }, updateOtherCharts({chart}: {chart: Chart}, reset: boolean = false) { - const minVal = chart.options.scales.x.min; - const maxVal = chart.options.scales.x.max; + const minVal = Number(chart.options.scales.x.min); + const maxVal = Number(chart.options.scales.x.max); if (reset) { this.xAxisMin = this.xAxisStart; this.xAxisMax = this.xAxisEnd; @@ -320,7 +321,7 @@ export default { }; }, }, -}; +}); </script> <template> <div> diff --git a/web_src/js/components/ScopedAccessTokenSelector.vue b/web_src/js/components/ScopedAccessTokenSelector.vue index 63214d0bf5..9eaf824035 100644 --- a/web_src/js/components/ScopedAccessTokenSelector.vue +++ b/web_src/js/components/ScopedAccessTokenSelector.vue @@ -35,7 +35,7 @@ onUnmounted(() => { document.querySelector('#scoped-access-submit').removeEventListener('click', onClickSubmit); }); -function onClickSubmit(e) { +function onClickSubmit(e: Event) { e.preventDefault(); const warningEl = document.querySelector('#scoped-access-warning'); diff --git a/web_src/js/features/admin/common.ts b/web_src/js/features/admin/common.ts index 6c725a3efe..b991749d81 100644 --- a/web_src/js/features/admin/common.ts +++ b/web_src/js/features/admin/common.ts @@ -90,7 +90,7 @@ export function initAdminCommon(): void { onOAuth2UseCustomURLChange(applyDefaultValues); } - function onOAuth2UseCustomURLChange(applyDefaultValues) { + function onOAuth2UseCustomURLChange(applyDefaultValues: boolean) { const provider = document.querySelector<HTMLInputElement>('#oauth2_provider').value; hideElem('.oauth2_use_custom_url_field'); for (const input of document.querySelectorAll<HTMLInputElement>('.oauth2_use_custom_url_field input[required]')) { diff --git a/web_src/js/features/citation.ts b/web_src/js/features/citation.ts index fc5bb38f0a..3c9fe0afc8 100644 --- a/web_src/js/features/citation.ts +++ b/web_src/js/features/citation.ts @@ -5,9 +5,13 @@ const {pageData} = window.config; async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) { const [{Cite, plugins}] = await Promise.all([ + // @ts-expect-error: module exports no types import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'), + // @ts-expect-error: module exports no types import(/* webpackChunkName: "citation-js-formats" */'@citation-js/plugin-software-formats'), + // @ts-expect-error: module exports no types import(/* webpackChunkName: "citation-js-bibtex" */'@citation-js/plugin-bibtex'), + // @ts-expect-error: module exports no types import(/* webpackChunkName: "citation-js-csl" */'@citation-js/plugin-csl'), ]); const {citationFileContent} = pageData; diff --git a/web_src/js/features/common-button.ts b/web_src/js/features/common-button.ts index 3162557b9b..7aebdd8dd5 100644 --- a/web_src/js/features/common-button.ts +++ b/web_src/js/features/common-button.ts @@ -74,10 +74,10 @@ export function initGlobalDeleteButton(): void { } } -function onShowPanelClick(e) { +function onShowPanelClick(e: MouseEvent) { // a '.show-panel' element can show a panel, by `data-panel="selector"` // if it has "toggle" class, it toggles the panel - const el = e.currentTarget; + const el = e.currentTarget as HTMLElement; e.preventDefault(); const sel = el.getAttribute('data-panel'); if (el.classList.contains('toggle')) { @@ -87,9 +87,9 @@ function onShowPanelClick(e) { } } -function onHidePanelClick(e) { +function onHidePanelClick(e: MouseEvent) { // a `.hide-panel` element can hide a panel, by `data-panel="selector"` or `data-panel-closest="selector"` - const el = e.currentTarget; + const el = e.currentTarget as HTMLElement; e.preventDefault(); let sel = el.getAttribute('data-panel'); if (sel) { @@ -98,13 +98,13 @@ function onHidePanelClick(e) { } sel = el.getAttribute('data-panel-closest'); if (sel) { - hideElem(el.parentNode.closest(sel)); + hideElem((el.parentNode as HTMLElement).closest(sel)); return; } throw new Error('no panel to hide'); // should never happen, otherwise there is a bug in code } -function onShowModalClick(e) { +function onShowModalClick(e: MouseEvent) { // A ".show-modal" button will show a modal dialog defined by its "data-modal" attribute. // Each "data-modal-{target}" attribute will be filled to target element's value or text-content. // * First, try to query '#target' @@ -112,7 +112,7 @@ function onShowModalClick(e) { // * Then, try to query '.target' // * Then, try to query 'target' as HTML tag // If there is a ".{attr}" part like "data-modal-form.action", then the form's "action" attribute will be set. - const el = e.currentTarget; + const el = e.currentTarget as HTMLElement; e.preventDefault(); const modalSelector = el.getAttribute('data-modal'); const elModal = document.querySelector(modalSelector); @@ -137,9 +137,9 @@ function onShowModalClick(e) { } if (attrTargetAttr) { - attrTarget[camelize(attrTargetAttr)] = attrib.value; + (attrTarget as any)[camelize(attrTargetAttr)] = attrib.value; } else if (attrTarget.matches('input, textarea')) { - attrTarget.value = attrib.value; // FIXME: add more supports like checkbox + (attrTarget as HTMLInputElement | HTMLTextAreaElement).value = attrib.value; // FIXME: add more supports like checkbox } else { attrTarget.textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p } diff --git a/web_src/js/features/common-fetch-action.ts b/web_src/js/features/common-fetch-action.ts index bc72f4089a..2da481e521 100644 --- a/web_src/js/features/common-fetch-action.ts +++ b/web_src/js/features/common-fetch-action.ts @@ -75,7 +75,10 @@ async function formFetchAction(formEl: HTMLFormElement, e: SubmitEvent) { } let reqUrl = formActionUrl; - const reqOpt = {method: formMethod.toUpperCase(), body: null}; + const reqOpt = { + method: formMethod.toUpperCase(), + body: null as FormData | null, + }; if (formMethod.toLowerCase() === 'get') { const params = new URLSearchParams(); for (const [key, value] of formData) { diff --git a/web_src/js/features/common-form.ts b/web_src/js/features/common-form.ts index 8532d397cd..7321d80c44 100644 --- a/web_src/js/features/common-form.ts +++ b/web_src/js/features/common-form.ts @@ -17,13 +17,13 @@ export function initGlobalEnterQuickSubmit() { if (e.key !== 'Enter') return; const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey); if (hasCtrlOrMeta && e.target.matches('textarea')) { - if (handleGlobalEnterQuickSubmit(e.target)) { + if (handleGlobalEnterQuickSubmit(e.target as HTMLElement)) { e.preventDefault(); } } else if (e.target.matches('input') && !e.target.closest('form')) { // input in a normal form could handle Enter key by default, so we only handle the input outside a form // eslint-disable-next-line unicorn/no-lonely-if - if (handleGlobalEnterQuickSubmit(e.target)) { + if (handleGlobalEnterQuickSubmit(e.target as HTMLElement)) { e.preventDefault(); } } diff --git a/web_src/js/features/common-organization.ts b/web_src/js/features/common-organization.ts index 47a61ece22..a1f19bedea 100644 --- a/web_src/js/features/common-organization.ts +++ b/web_src/js/features/common-organization.ts @@ -6,7 +6,7 @@ export function initCommonOrganization() { return; } - document.querySelector('.organization.settings.options #org_name')?.addEventListener('input', function () { + document.querySelector<HTMLInputElement>('.organization.settings.options #org_name')?.addEventListener('input', function () { const nameChanged = this.value.toLowerCase() !== this.getAttribute('data-org-name').toLowerCase(); toggleElem('#org-name-change-prompt', nameChanged); }); diff --git a/web_src/js/features/comp/ComboMarkdownEditor.ts b/web_src/js/features/comp/ComboMarkdownEditor.ts index bba50a1296..d3773a89c4 100644 --- a/web_src/js/features/comp/ComboMarkdownEditor.ts +++ b/web_src/js/features/comp/ComboMarkdownEditor.ts @@ -29,10 +29,10 @@ let elementIdCounter = 0; /** * validate if the given textarea is non-empty. - * @param {HTMLElement} textarea - The textarea element to be validated. + * @param {HTMLTextAreaElement} textarea - The textarea element to be validated. * @returns {boolean} returns true if validation succeeded. */ -export function validateTextareaNonEmpty(textarea) { +export function validateTextareaNonEmpty(textarea: HTMLTextAreaElement) { // When using EasyMDE, the original edit area HTML element is hidden, breaking HTML5 input validation. // The workaround (https://github.com/sparksuite/simplemde-markdown-editor/issues/324) doesn't work with contenteditable, so we just show an alert. if (!textarea.value) { @@ -49,16 +49,25 @@ export function validateTextareaNonEmpty(textarea) { return true; } +type Heights = { + minHeight?: string, + height?: string, + maxHeight?: string, +}; + type ComboMarkdownEditorOptions = { - editorHeights?: {minHeight?: string, height?: string, maxHeight?: string}, + editorHeights?: Heights, easyMDEOptions?: EasyMDE.Options, }; +type ComboMarkdownEditorTextarea = HTMLTextAreaElement & {_giteaComboMarkdownEditor: any}; +type ComboMarkdownEditorContainer = HTMLElement & {_giteaComboMarkdownEditor?: any}; + export class ComboMarkdownEditor { static EventEditorContentChanged = EventEditorContentChanged; static EventUploadStateChanged = EventUploadStateChanged; - public container : HTMLElement; + public container: HTMLElement; options: ComboMarkdownEditorOptions; @@ -70,7 +79,7 @@ export class ComboMarkdownEditor { easyMDEToolbarActions: any; easyMDEToolbarDefault: any; - textarea: HTMLTextAreaElement & {_giteaComboMarkdownEditor: any}; + textarea: ComboMarkdownEditorTextarea; textareaMarkdownToolbar: HTMLElement; textareaAutosize: any; @@ -81,7 +90,7 @@ export class ComboMarkdownEditor { previewUrl: string; previewContext: string; - constructor(container, options:ComboMarkdownEditorOptions = {}) { + constructor(container: ComboMarkdownEditorContainer, options:ComboMarkdownEditorOptions = {}) { if (container._giteaComboMarkdownEditor) throw new Error('ComboMarkdownEditor already initialized'); container._giteaComboMarkdownEditor = this; this.options = options; @@ -98,7 +107,7 @@ export class ComboMarkdownEditor { await this.switchToUserPreference(); } - applyEditorHeights(el, heights) { + applyEditorHeights(el: HTMLElement, heights: Heights) { if (!heights) return; if (heights.minHeight) el.style.minHeight = heights.minHeight; if (heights.height) el.style.height = heights.height; @@ -283,7 +292,7 @@ export class ComboMarkdownEditor { ]; } - parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions) { + parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions: any) { this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(easyMde, this); const processed = []; for (const action of actions) { @@ -332,21 +341,21 @@ export class ComboMarkdownEditor { this.easyMDE = new EasyMDE(easyMDEOpt); this.easyMDE.codemirror.on('change', () => triggerEditorContentChanged(this.container)); this.easyMDE.codemirror.setOption('extraKeys', { - 'Cmd-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()), - 'Ctrl-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()), - Enter: (cm) => { + 'Cmd-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()), + 'Ctrl-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()), + Enter: (cm: any) => { const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); if (!tributeContainer || tributeContainer.style.display === 'none') { cm.execCommand('newlineAndIndent'); } }, - Up: (cm) => { + Up: (cm: any) => { const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); if (!tributeContainer || tributeContainer.style.display === 'none') { return cm.execCommand('goLineUp'); } }, - Down: (cm) => { + Down: (cm: any) => { const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); if (!tributeContainer || tributeContainer.style.display === 'none') { return cm.execCommand('goLineDown'); @@ -354,14 +363,14 @@ export class ComboMarkdownEditor { }, }); this.applyEditorHeights(this.container.querySelector('.CodeMirror-scroll'), this.options.editorHeights); - await attachTribute(this.easyMDE.codemirror.getInputField(), {mentions: true, emoji: true}); + await attachTribute(this.easyMDE.codemirror.getInputField()); if (this.dropzone) { initEasyMDEPaste(this.easyMDE, this.dropzone); } hideElem(this.textareaMarkdownToolbar); } - value(v = undefined) { + value(v: any = undefined) { if (v === undefined) { if (this.easyMDE) { return this.easyMDE.value(); @@ -402,7 +411,7 @@ export class ComboMarkdownEditor { } } -export function getComboMarkdownEditor(el) { +export function getComboMarkdownEditor(el: any) { if (!el) return null; if (el.length) el = el[0]; return el._giteaComboMarkdownEditor; diff --git a/web_src/js/features/comp/EditorMarkdown.ts b/web_src/js/features/comp/EditorMarkdown.ts index d3ed492396..6e66c15763 100644 --- a/web_src/js/features/comp/EditorMarkdown.ts +++ b/web_src/js/features/comp/EditorMarkdown.ts @@ -1,10 +1,10 @@ export const EventEditorContentChanged = 'ce-editor-content-changed'; -export function triggerEditorContentChanged(target) { +export function triggerEditorContentChanged(target: HTMLElement) { target.dispatchEvent(new CustomEvent(EventEditorContentChanged, {bubbles: true})); } -export function textareaInsertText(textarea, value) { +export function textareaInsertText(textarea: HTMLTextAreaElement, value: string) { const startPos = textarea.selectionStart; const endPos = textarea.selectionEnd; textarea.value = textarea.value.substring(0, startPos) + value + textarea.value.substring(endPos); @@ -20,7 +20,7 @@ type TextareaValueSelection = { selEnd: number; } -function handleIndentSelection(textarea: HTMLTextAreaElement, e) { +function handleIndentSelection(textarea: HTMLTextAreaElement, e: KeyboardEvent) { const selStart = textarea.selectionStart; const selEnd = textarea.selectionEnd; if (selEnd === selStart) return; // do not process when no selection @@ -184,8 +184,13 @@ function handleNewline(textarea: HTMLTextAreaElement, e: Event) { triggerEditorContentChanged(textarea); } -export function initTextareaMarkdown(textarea) { +function isTextExpanderShown(textarea: HTMLElement): boolean { + return Boolean(textarea.closest('text-expander')?.querySelector('.suggestions')); +} + +export function initTextareaMarkdown(textarea: HTMLTextAreaElement) { textarea.addEventListener('keydown', (e) => { + if (isTextExpanderShown(textarea)) return; if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) { // use Tab/Shift-Tab to indent/unindent the selected lines handleIndentSelection(textarea, e); diff --git a/web_src/js/features/comp/EditorUpload.ts b/web_src/js/features/comp/EditorUpload.ts index 89982747ea..f6d5731422 100644 --- a/web_src/js/features/comp/EditorUpload.ts +++ b/web_src/js/features/comp/EditorUpload.ts @@ -8,43 +8,46 @@ import { generateMarkdownLinkForAttachment, } from '../dropzone.ts'; import type CodeMirror from 'codemirror'; +import type EasyMDE from 'easymde'; +import type {DropzoneFile} from 'dropzone'; let uploadIdCounter = 0; export const EventUploadStateChanged = 'ce-upload-state-changed'; -export function triggerUploadStateChanged(target) { +export function triggerUploadStateChanged(target: HTMLElement) { target.dispatchEvent(new CustomEvent(EventUploadStateChanged, {bubbles: true})); } -function uploadFile(dropzoneEl, file) { +function uploadFile(dropzoneEl: HTMLElement, file: File) { return new Promise((resolve) => { const curUploadId = uploadIdCounter++; - file._giteaUploadId = curUploadId; + (file as any)._giteaUploadId = curUploadId; const dropzoneInst = dropzoneEl.dropzone; - const onUploadDone = ({file}) => { + const onUploadDone = ({file}: {file: any}) => { if (file._giteaUploadId === curUploadId) { dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone); resolve(file); } }; dropzoneInst.on(DropzoneCustomEventUploadDone, onUploadDone); - dropzoneInst.handleFiles([file]); + // FIXME: this is not entirely correct because `file` does not satisfy DropzoneFile (we have abused the Dropzone for long time) + dropzoneInst.addFile(file as DropzoneFile); }); } class TextareaEditor { - editor : HTMLTextAreaElement; + editor: HTMLTextAreaElement; - constructor(editor) { + constructor(editor: HTMLTextAreaElement) { this.editor = editor; } - insertPlaceholder(value) { + insertPlaceholder(value: string) { textareaInsertText(this.editor, value); } - replacePlaceholder(oldVal, newVal) { + replacePlaceholder(oldVal: string, newVal: string) { const editor = this.editor; const startPos = editor.selectionStart; const endPos = editor.selectionEnd; @@ -65,11 +68,11 @@ class TextareaEditor { class CodeMirrorEditor { editor: CodeMirror.EditorFromTextArea; - constructor(editor) { + constructor(editor: CodeMirror.EditorFromTextArea) { this.editor = editor; } - insertPlaceholder(value) { + insertPlaceholder(value: string) { const editor = this.editor; const startPoint = editor.getCursor('start'); const endPoint = editor.getCursor('end'); @@ -80,7 +83,7 @@ class CodeMirrorEditor { triggerEditorContentChanged(editor.getTextArea()); } - replacePlaceholder(oldVal, newVal) { + replacePlaceholder(oldVal: string, newVal: string) { const editor = this.editor; const endPoint = editor.getCursor('end'); if (editor.getSelection() === oldVal) { @@ -96,7 +99,7 @@ class CodeMirrorEditor { } } -async function handleUploadFiles(editor, dropzoneEl, files, e) { +async function handleUploadFiles(editor: CodeMirrorEditor | TextareaEditor, dropzoneEl: HTMLElement, files: Array<File> | FileList, e: Event) { e.preventDefault(); for (const file of files) { const name = file.name.slice(0, file.name.lastIndexOf('.')); @@ -109,13 +112,13 @@ async function handleUploadFiles(editor, dropzoneEl, files, e) { } } -export function removeAttachmentLinksFromMarkdown(text, fileUuid) { +export function removeAttachmentLinksFromMarkdown(text: string, fileUuid: string) { text = text.replace(new RegExp(`!?\\[([^\\]]+)\\]\\(/?attachments/${fileUuid}\\)`, 'g'), ''); text = text.replace(new RegExp(`<img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), ''); return text; } -function handleClipboardText(textarea, e, {text, isShiftDown}) { +function handleClipboardText(textarea: HTMLTextAreaElement, e: ClipboardEvent, text: string, isShiftDown: boolean) { // pasting with "shift" means "paste as original content" in most applications if (isShiftDown) return; // let the browser handle it @@ -131,7 +134,7 @@ function handleClipboardText(textarea, e, {text, isShiftDown}) { } // extract text and images from "paste" event -function getPastedContent(e) { +function getPastedContent(e: ClipboardEvent) { const images = []; for (const item of e.clipboardData?.items ?? []) { if (item.type?.startsWith('image/')) { @@ -142,8 +145,8 @@ function getPastedContent(e) { return {text, images}; } -export function initEasyMDEPaste(easyMDE, dropzoneEl) { - const editor = new CodeMirrorEditor(easyMDE.codemirror); +export function initEasyMDEPaste(easyMDE: EasyMDE, dropzoneEl: HTMLElement) { + const editor = new CodeMirrorEditor(easyMDE.codemirror as any); easyMDE.codemirror.on('paste', (_, e) => { const {images} = getPastedContent(e); if (!images.length) return; @@ -160,28 +163,28 @@ export function initEasyMDEPaste(easyMDE, dropzoneEl) { }); } -export function initTextareaEvents(textarea, dropzoneEl) { +export function initTextareaEvents(textarea: HTMLTextAreaElement, dropzoneEl: HTMLElement) { let isShiftDown = false; - textarea.addEventListener('keydown', (e) => { + textarea.addEventListener('keydown', (e: KeyboardEvent) => { if (e.shiftKey) isShiftDown = true; }); - textarea.addEventListener('keyup', (e) => { + textarea.addEventListener('keyup', (e: KeyboardEvent) => { if (!e.shiftKey) isShiftDown = false; }); - textarea.addEventListener('paste', (e) => { + textarea.addEventListener('paste', (e: ClipboardEvent) => { const {images, text} = getPastedContent(e); if (images.length && dropzoneEl) { handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, images, e); } else if (text) { - handleClipboardText(textarea, e, {text, isShiftDown}); + handleClipboardText(textarea, e, text, isShiftDown); } }); - textarea.addEventListener('drop', (e) => { + textarea.addEventListener('drop', (e: DragEvent) => { if (!e.dataTransfer.files.length) return; if (!dropzoneEl) return; handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, e.dataTransfer.files, e); }); - dropzoneEl?.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}) => { + dropzoneEl?.dropzone.on(DropzoneCustomEventRemovedFile, ({fileUuid}: {fileUuid: string}) => { const newText = removeAttachmentLinksFromMarkdown(textarea.value, fileUuid); if (textarea.value !== newText) textarea.value = newText; }); diff --git a/web_src/js/features/comp/QuickSubmit.ts b/web_src/js/features/comp/QuickSubmit.ts index 385acb319f..0a41f69132 100644 --- a/web_src/js/features/comp/QuickSubmit.ts +++ b/web_src/js/features/comp/QuickSubmit.ts @@ -1,6 +1,6 @@ import {querySingleVisibleElem} from '../../utils/dom.ts'; -export function handleGlobalEnterQuickSubmit(target) { +export function handleGlobalEnterQuickSubmit(target: HTMLElement) { let form = target.closest('form'); if (form) { if (!form.checkValidity()) { diff --git a/web_src/js/features/comp/SearchUserBox.ts b/web_src/js/features/comp/SearchUserBox.ts index 2e3b3f83be..9fedb3ed24 100644 --- a/web_src/js/features/comp/SearchUserBox.ts +++ b/web_src/js/features/comp/SearchUserBox.ts @@ -14,7 +14,7 @@ export function initCompSearchUserBox() { minCharacters: 2, apiSettings: { url: `${appSubUrl}/user/search_candidates?q={query}`, - onResponse(response) { + onResponse(response: any) { const resultItems = []; const searchQuery = searchUserBox.querySelector('input').value; const searchQueryUppercase = searchQuery.toUpperCase(); diff --git a/web_src/js/features/comp/TextExpander.ts b/web_src/js/features/comp/TextExpander.ts index e0c4abed75..dc08d1739d 100644 --- a/web_src/js/features/comp/TextExpander.ts +++ b/web_src/js/features/comp/TextExpander.ts @@ -1,14 +1,20 @@ import {matchEmoji, matchMention, matchIssue} from '../../utils/match.ts'; import {emojiString} from '../emoji.ts'; import {svg} from '../../svg.ts'; -import {parseIssueHref, parseIssueNewHref} from '../../utils.ts'; +import {parseIssueHref, parseRepoOwnerPathInfo} from '../../utils.ts'; import {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts'; import {getIssueColor, getIssueIcon} from '../issue.ts'; import {debounce} from 'perfect-debounce'; +import type TextExpanderElement from '@github/text-expander-element'; const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => { - let issuePathInfo = parseIssueHref(window.location.href); - if (!issuePathInfo.ownerName) issuePathInfo = parseIssueNewHref(window.location.href); + const issuePathInfo = parseIssueHref(window.location.href); + if (!issuePathInfo.ownerName) { + const repoOwnerPathInfo = parseRepoOwnerPathInfo(window.location.pathname); + issuePathInfo.ownerName = repoOwnerPathInfo.ownerName; + issuePathInfo.repoName = repoOwnerPathInfo.repoName; + // then no issuePathInfo.indexString here, it is only used to exclude the current issue when "matchIssue" + } if (!issuePathInfo.ownerName) return resolve({matched: false}); const matches = await matchIssue(issuePathInfo.ownerName, issuePathInfo.repoName, issuePathInfo.indexString, text); @@ -27,8 +33,8 @@ const debouncedSuggestIssues = debounce((key: string, text: string) => new Promi resolve({matched: true, fragment: ul}); }), 100); -export function initTextExpander(expander) { - expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => { +export function initTextExpander(expander: TextExpanderElement) { + expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}: Record<string, any>) => { if (key === ':') { const matches = matchEmoji(text); if (!matches.length) return provide({matched: false}); @@ -79,7 +85,7 @@ export function initTextExpander(expander) { provide(debouncedSuggestIssues(key, text)); } }); - expander?.addEventListener('text-expander-value', ({detail}) => { + expander?.addEventListener('text-expander-value', ({detail}: Record<string, any>) => { if (detail?.item) { // add a space after @mentions and #issue as it's likely the user wants one const suffix = ['@', '#'].includes(detail.key) ? ' ' : ''; diff --git a/web_src/js/features/comp/WebHookEditor.ts b/web_src/js/features/comp/WebHookEditor.ts index 203396af80..794b3c99ca 100644 --- a/web_src/js/features/comp/WebHookEditor.ts +++ b/web_src/js/features/comp/WebHookEditor.ts @@ -6,7 +6,7 @@ export function initCompWebHookEditor() { return; } - for (const input of document.querySelectorAll('.events.checkbox input')) { + for (const input of document.querySelectorAll<HTMLInputElement>('.events.checkbox input')) { input.addEventListener('change', function () { if (this.checked) { showElem('.events.fields'); @@ -14,7 +14,7 @@ export function initCompWebHookEditor() { }); } - for (const input of document.querySelectorAll('.non-events.checkbox input')) { + for (const input of document.querySelectorAll<HTMLInputElement>('.non-events.checkbox input')) { input.addEventListener('change', function () { if (this.checked) { hideElem('.events.fields'); @@ -34,7 +34,7 @@ export function initCompWebHookEditor() { } // Test delivery - document.querySelector('#test-delivery')?.addEventListener('click', async function () { + document.querySelector<HTMLButtonElement>('#test-delivery')?.addEventListener('click', async function () { this.classList.add('is-loading', 'disabled'); await POST(this.getAttribute('data-link')); setTimeout(() => { diff --git a/web_src/js/features/contextpopup.ts b/web_src/js/features/contextpopup.ts index 33eead8431..7477331dbe 100644 --- a/web_src/js/features/contextpopup.ts +++ b/web_src/js/features/contextpopup.ts @@ -4,11 +4,11 @@ import {parseIssueHref} from '../utils.ts'; import {createTippy} from '../modules/tippy.ts'; export function initContextPopups() { - const refIssues = document.querySelectorAll('.ref-issue'); + const refIssues = document.querySelectorAll<HTMLElement>('.ref-issue'); attachRefIssueContextPopup(refIssues); } -export function attachRefIssueContextPopup(refIssues) { +export function attachRefIssueContextPopup(refIssues: NodeListOf<HTMLElement>) { for (const refIssue of refIssues) { if (refIssue.classList.contains('ref-external-issue')) continue; diff --git a/web_src/js/features/copycontent.ts b/web_src/js/features/copycontent.ts index af867463b2..4bc9281a35 100644 --- a/web_src/js/features/copycontent.ts +++ b/web_src/js/features/copycontent.ts @@ -46,7 +46,7 @@ export function initCopyContent() { showTemporaryTooltip(btn, i18n.copy_success); } else { if (isRasterImage) { - const success = await clippie(await convertImage(content, 'image/png')); + const success = await clippie(await convertImage(content as Blob, 'image/png')); showTemporaryTooltip(btn, success ? i18n.copy_success : i18n.copy_error); } else { showTemporaryTooltip(btn, i18n.copy_error); diff --git a/web_src/js/features/dashboard.ts b/web_src/js/features/dashboard.ts new file mode 100644 index 0000000000..71a0df3a64 --- /dev/null +++ b/web_src/js/features/dashboard.ts @@ -0,0 +1,9 @@ +import {createApp} from 'vue'; +import DashboardRepoList from '../components/DashboardRepoList.vue'; + +export function initDashboardRepoList() { + const el = document.querySelector('#dashboard-repo-list'); + if (el) { + createApp(DashboardRepoList).mount(el); + } +} diff --git a/web_src/js/features/dropzone.ts b/web_src/js/features/dropzone.ts index 666c645230..b2ba7651c4 100644 --- a/web_src/js/features/dropzone.ts +++ b/web_src/js/features/dropzone.ts @@ -6,16 +6,18 @@ import {GET, POST} from '../modules/fetch.ts'; import {showErrorToast} from '../modules/toast.ts'; import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts'; import {isImageFile, isVideoFile} from '../utils.ts'; -import type {DropzoneFile} from 'dropzone/index.js'; +import type {DropzoneFile, DropzoneOptions} from 'dropzone/index.js'; const {csrfToken, i18n} = window.config; +type CustomDropzoneFile = DropzoneFile & {uuid: string}; + // dropzone has its owner event dispatcher (emitter) export const DropzoneCustomEventReloadFiles = 'dropzone-custom-reload-files'; export const DropzoneCustomEventRemovedFile = 'dropzone-custom-removed-file'; export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done'; -async function createDropzone(el, opts) { +async function createDropzone(el: HTMLElement, opts: DropzoneOptions) { const [{default: Dropzone}] = await Promise.all([ import(/* webpackChunkName: "dropzone" */'dropzone'), import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'), @@ -23,7 +25,7 @@ async function createDropzone(el, opts) { return new Dropzone(el, opts); } -export function generateMarkdownLinkForAttachment(file, {width, dppx}: {width?: number, dppx?: number} = {}) { +export function generateMarkdownLinkForAttachment(file: Partial<CustomDropzoneFile>, {width, dppx}: {width?: number, dppx?: number} = {}) { let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`; if (isImageFile(file)) { fileMarkdown = `!${fileMarkdown}`; @@ -43,7 +45,7 @@ export function generateMarkdownLinkForAttachment(file, {width, dppx}: {width?: return fileMarkdown; } -function addCopyLink(file) { +function addCopyLink(file: Partial<CustomDropzoneFile>) { // Create a "Copy Link" element, to conveniently copy the image or file link as Markdown to the clipboard // The "<a>" element has a hardcoded cursor: pointer because the default is overridden by .dropzone const copyLinkEl = createElementFromHTML(` @@ -58,6 +60,8 @@ function addCopyLink(file) { file.previewTemplate.append(copyLinkEl); } +type FileUuidDict = Record<string, {submitted: boolean}>; + /** * @param {HTMLElement} dropzoneEl */ @@ -67,7 +71,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) { const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url'); let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event - let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone + let fileUuidDict: FileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone const opts: Record<string, any> = { url: dropzoneEl.getAttribute('data-upload-url'), headers: {'X-Csrf-Token': csrfToken}, @@ -89,7 +93,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) { // "http://localhost:3000/owner/repo/issues/[object%20Event]" // the reason is that the preview "callback(dataURL)" is assign to "img.onerror" then "thumbnail" uses the error object as the dataURL and generates '<img src="[object Event]">' const dzInst = await createDropzone(dropzoneEl, opts); - dzInst.on('success', (file: DropzoneFile & {uuid: string}, resp: any) => { + dzInst.on('success', (file: CustomDropzoneFile, resp: any) => { file.uuid = resp.uuid; fileUuidDict[file.uuid] = {submitted: false}; const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${resp.uuid}`, value: resp.uuid}); @@ -98,7 +102,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) { dzInst.emit(DropzoneCustomEventUploadDone, {file}); }); - dzInst.on('removedfile', async (file: DropzoneFile & {uuid: string}) => { + dzInst.on('removedfile', async (file: CustomDropzoneFile) => { if (disableRemovedfileEvent) return; dzInst.emit(DropzoneCustomEventRemovedFile, {fileUuid: file.uuid}); diff --git a/web_src/js/features/emoji.ts b/web_src/js/features/emoji.ts index 933aa951c5..135620e51e 100644 --- a/web_src/js/features/emoji.ts +++ b/web_src/js/features/emoji.ts @@ -15,13 +15,13 @@ export const emojiKeys = Object.keys(tempMap).sort((a, b) => { return a.localeCompare(b); }); -const emojiMap = {}; +const emojiMap: Record<string, string> = {}; for (const key of emojiKeys) { emojiMap[key] = tempMap[key]; } // retrieve HTML for given emoji name -export function emojiHTML(name) { +export function emojiHTML(name: string) { let inner; if (Object.hasOwn(customEmojis, name)) { inner = `<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`; @@ -33,6 +33,6 @@ export function emojiHTML(name) { } // retrieve string for given emoji name -export function emojiString(name) { +export function emojiString(name: string) { return emojiMap[name] || `:${name}:`; } diff --git a/web_src/js/features/file-fold.ts b/web_src/js/features/file-fold.ts index 6fe068341a..19950d9b9f 100644 --- a/web_src/js/features/file-fold.ts +++ b/web_src/js/features/file-fold.ts @@ -5,15 +5,15 @@ import {svg} from '../svg.ts'; // The fold arrow is the icon displayed on the upper left of the file box, especially intended for components having the 'fold-file' class. // The file content box is the box that should be hidden or shown, especially intended for components having the 'file-content' class. // -export function setFileFolding(fileContentBox, foldArrow, newFold) { +export function setFileFolding(fileContentBox: HTMLElement, foldArrow: HTMLElement, newFold: boolean) { foldArrow.innerHTML = svg(`octicon-chevron-${newFold ? 'right' : 'down'}`, 18); - fileContentBox.setAttribute('data-folded', newFold); + fileContentBox.setAttribute('data-folded', String(newFold)); if (newFold && fileContentBox.getBoundingClientRect().top < 0) { fileContentBox.scrollIntoView(); } } // Like `setFileFolding`, except that it automatically inverts the current file folding state. -export function invertFileFolding(fileContentBox, foldArrow) { +export function invertFileFolding(fileContentBox:HTMLElement, foldArrow: HTMLElement) { setFileFolding(fileContentBox, foldArrow, fileContentBox.getAttribute('data-folded') !== 'true'); } diff --git a/web_src/js/features/heatmap.ts b/web_src/js/features/heatmap.ts index 53eebc93e5..7cec82108b 100644 --- a/web_src/js/features/heatmap.ts +++ b/web_src/js/features/heatmap.ts @@ -7,7 +7,7 @@ export function initHeatmap() { if (!el) return; try { - const heatmap = {}; + const heatmap: Record<string, number> = {}; for (const {contributions, timestamp} of JSON.parse(el.getAttribute('data-heatmap-data'))) { // Convert to user timezone and sum contributions by date const dateStr = new Date(timestamp * 1000).toDateString(); diff --git a/web_src/js/features/imagediff.ts b/web_src/js/features/imagediff.ts index cd61888f83..e62734293a 100644 --- a/web_src/js/features/imagediff.ts +++ b/web_src/js/features/imagediff.ts @@ -3,7 +3,7 @@ import {hideElem, loadElem, queryElemChildren, queryElems} from '../utils/dom.ts import {parseDom} from '../utils.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; -function getDefaultSvgBoundsIfUndefined(text, src) { +function getDefaultSvgBoundsIfUndefined(text: string, src: string) { const defaultSize = 300; const maxSize = 99999; @@ -38,7 +38,7 @@ function getDefaultSvgBoundsIfUndefined(text, src) { return null; } -function createContext(imageAfter, imageBefore) { +function createContext(imageAfter: HTMLImageElement, imageBefore: HTMLImageElement) { const sizeAfter = { width: imageAfter?.width || 0, height: imageAfter?.height || 0, @@ -123,7 +123,7 @@ class ImageDiff { queryElemChildren(containerEl, '.image-diff-tabs', (el) => el.classList.remove('is-loading')); } - initSideBySide(sizes) { + initSideBySide(sizes: Record<string, any>) { let factor = 1; if (sizes.maxSize.width > (this.diffContainerWidth - 24) / 2) { factor = (this.diffContainerWidth - 24) / 2 / sizes.maxSize.width; @@ -176,7 +176,7 @@ class ImageDiff { } } - initSwipe(sizes) { + initSwipe(sizes: Record<string, any>) { let factor = 1; if (sizes.maxSize.width > this.diffContainerWidth - 12) { factor = (this.diffContainerWidth - 12) / sizes.maxSize.width; @@ -215,14 +215,14 @@ class ImageDiff { this.containerEl.querySelector('.swipe-bar').addEventListener('mousedown', (e) => { e.preventDefault(); - this.initSwipeEventListeners(e.currentTarget); + this.initSwipeEventListeners(e.currentTarget as HTMLElement); }); } - initSwipeEventListeners(swipeBar) { - const swipeFrame = swipeBar.parentNode; + initSwipeEventListeners(swipeBar: HTMLElement) { + const swipeFrame = swipeBar.parentNode as HTMLElement; const width = swipeFrame.clientWidth; - const onSwipeMouseMove = (e) => { + const onSwipeMouseMove = (e: MouseEvent) => { e.preventDefault(); const rect = swipeFrame.getBoundingClientRect(); const value = Math.max(0, Math.min(e.clientX - rect.left, width)); @@ -237,7 +237,7 @@ class ImageDiff { document.addEventListener('mouseup', removeEventListeners); } - initOverlay(sizes) { + initOverlay(sizes: Record<string, any>) { let factor = 1; if (sizes.maxSize.width > this.diffContainerWidth - 12) { factor = (this.diffContainerWidth - 12) / sizes.maxSize.width; diff --git a/web_src/js/features/install.ts b/web_src/js/features/install.ts index 725dcafab0..34df4757f9 100644 --- a/web_src/js/features/install.ts +++ b/web_src/js/features/install.ts @@ -12,11 +12,12 @@ export function initInstall() { initPreInstall(); } } + function initPreInstall() { const defaultDbUser = 'gitea'; const defaultDbName = 'gitea'; - const defaultDbHosts = { + const defaultDbHosts: Record<string, string> = { mysql: '127.0.0.1:3306', postgres: '127.0.0.1:5432', mssql: '127.0.0.1:1433', @@ -27,7 +28,7 @@ function initPreInstall() { const dbName = document.querySelector<HTMLInputElement>('#db_name'); // Database type change detection. - document.querySelector('#db_type').addEventListener('change', function () { + document.querySelector<HTMLInputElement>('#db_type').addEventListener('change', function () { const dbType = this.value; hideElem('div[data-db-setting-for]'); showElem(`div[data-db-setting-for=${dbType}]`); @@ -59,26 +60,26 @@ function initPreInstall() { } // TODO: better handling of exclusive relations. - document.querySelector('#offline-mode input').addEventListener('change', function () { + document.querySelector<HTMLInputElement>('#offline-mode input').addEventListener('change', function () { if (this.checked) { document.querySelector<HTMLInputElement>('#disable-gravatar input').checked = true; document.querySelector<HTMLInputElement>('#federated-avatar-lookup input').checked = false; } }); - document.querySelector('#disable-gravatar input').addEventListener('change', function () { + document.querySelector<HTMLInputElement>('#disable-gravatar input').addEventListener('change', function () { if (this.checked) { document.querySelector<HTMLInputElement>('#federated-avatar-lookup input').checked = false; } else { document.querySelector<HTMLInputElement>('#offline-mode input').checked = false; } }); - document.querySelector('#federated-avatar-lookup input').addEventListener('change', function () { + document.querySelector<HTMLInputElement>('#federated-avatar-lookup input').addEventListener('change', function () { if (this.checked) { document.querySelector<HTMLInputElement>('#disable-gravatar input').checked = false; document.querySelector<HTMLInputElement>('#offline-mode input').checked = false; } }); - document.querySelector('#enable-openid-signin input').addEventListener('change', function () { + document.querySelector<HTMLInputElement>('#enable-openid-signin input').addEventListener('change', function () { if (this.checked) { if (!document.querySelector<HTMLInputElement>('#disable-registration input').checked) { document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = true; @@ -87,7 +88,7 @@ function initPreInstall() { document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = false; } }); - document.querySelector('#disable-registration input').addEventListener('change', function () { + document.querySelector<HTMLInputElement>('#disable-registration input').addEventListener('change', function () { if (this.checked) { document.querySelector<HTMLInputElement>('#enable-captcha input').checked = false; document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = false; @@ -95,7 +96,7 @@ function initPreInstall() { document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = true; } }); - document.querySelector('#enable-captcha input').addEventListener('change', function () { + document.querySelector<HTMLInputElement>('#enable-captcha input').addEventListener('change', function () { if (this.checked) { document.querySelector<HTMLInputElement>('#disable-registration input').checked = false; } diff --git a/web_src/js/features/issue.ts b/web_src/js/features/issue.ts index a56015a2a2..911cf713d9 100644 --- a/web_src/js/features/issue.ts +++ b/web_src/js/features/issue.ts @@ -1,17 +1,21 @@ import type {Issue} from '../types.ts'; +// the getIssueIcon/getIssueColor logic should be kept the same as "templates/shared/issueicon.tmpl" + export function getIssueIcon(issue: Issue) { if (issue.pull_request) { if (issue.state === 'open') { - if (issue.pull_request.draft === true) { + if (issue.pull_request.draft) { return 'octicon-git-pull-request-draft'; // WIP PR } return 'octicon-git-pull-request'; // Open PR - } else if (issue.pull_request.merged === true) { + } else if (issue.pull_request.merged) { return 'octicon-git-merge'; // Merged PR } - return 'octicon-git-pull-request'; // Closed PR - } else if (issue.state === 'open') { + return 'octicon-git-pull-request-closed'; // Closed PR + } + + if (issue.state === 'open') { return 'octicon-issue-opened'; // Open Issue } return 'octicon-issue-closed'; // Closed Issue @@ -19,12 +23,17 @@ export function getIssueIcon(issue: Issue) { export function getIssueColor(issue: Issue) { if (issue.pull_request) { - if (issue.pull_request.draft === true) { - return 'grey'; // WIP PR - } else if (issue.pull_request.merged === true) { + if (issue.state === 'open') { + if (issue.pull_request.draft) { + return 'grey'; // WIP PR + } + return 'green'; // Open PR + } else if (issue.pull_request.merged) { return 'purple'; // Merged PR } + return 'red'; // Closed PR } + if (issue.state === 'open') { return 'green'; // Open Issue } diff --git a/web_src/js/features/org-team.ts b/web_src/js/features/org-team.ts index e160f07bf2..d07818b0ac 100644 --- a/web_src/js/features/org-team.ts +++ b/web_src/js/features/org-team.ts @@ -21,7 +21,7 @@ function initOrgTeamSearchRepoBox() { minCharacters: 2, apiSettings: { url: `${appSubUrl}/repo/search?q={query}&uid=${$searchRepoBox.data('uid')}`, - onResponse(response) { + onResponse(response: any) { const items = []; for (const item of response.data) { items.push({ diff --git a/web_src/js/features/pull-view-file.ts b/web_src/js/features/pull-view-file.ts index 36fe4bc4df..16ccf00084 100644 --- a/web_src/js/features/pull-view-file.ts +++ b/web_src/js/features/pull-view-file.ts @@ -38,7 +38,7 @@ export function initViewedCheckboxListenerFor() { // The checkbox consists of a div containing the real checkbox with its label and the CSRF token, // hence the actual checkbox first has to be found - const checkbox = form.querySelector('input[type=checkbox]'); + const checkbox = form.querySelector<HTMLInputElement>('input[type=checkbox]'); checkbox.addEventListener('input', function() { // Mark the file as viewed visually - will especially change the background if (this.checked) { @@ -59,13 +59,13 @@ export function initViewedCheckboxListenerFor() { const fileName = checkbox.getAttribute('name'); // check if the file is in our difftreestore and if we find it -> change the IsViewed status - const fileInPageData = diffTreeStore().files.find((x) => x.Name === fileName); + const fileInPageData = diffTreeStore().files.find((x: Record<string, any>) => x.Name === fileName); if (fileInPageData) { fileInPageData.IsViewed = this.checked; } // Unfortunately, actual forms cause too many problems, hence another approach is needed - const files = {}; + const files: Record<string, boolean> = {}; files[fileName] = this.checked; const data: Record<string, any> = {files}; const headCommitSHA = form.getAttribute('data-headcommit'); @@ -82,13 +82,13 @@ export function initViewedCheckboxListenerFor() { export function initExpandAndCollapseFilesButton() { // expand btn document.querySelector(expandFilesBtnSelector)?.addEventListener('click', () => { - for (const box of document.querySelectorAll('.file-content[data-folded="true"]')) { + for (const box of document.querySelectorAll<HTMLElement>('.file-content[data-folded="true"]')) { setFileFolding(box, box.querySelector('.fold-file'), false); } }); // collapse btn, need to exclude the div of “show more” document.querySelector(collapseFilesBtnSelector)?.addEventListener('click', () => { - for (const box of document.querySelectorAll('.file-content:not([data-folded="true"])')) { + for (const box of document.querySelectorAll<HTMLElement>('.file-content:not([data-folded="true"])')) { if (box.getAttribute('id') === 'diff-incomplete') continue; setFileFolding(box, box.querySelector('.fold-file'), true); } diff --git a/web_src/js/features/repo-actions.ts b/web_src/js/features/repo-actions.ts new file mode 100644 index 0000000000..cbd0429c04 --- /dev/null +++ b/web_src/js/features/repo-actions.ts @@ -0,0 +1,47 @@ +import {createApp} from 'vue'; +import RepoActionView from '../components/RepoActionView.vue'; + +export function initRepositoryActionView() { + const el = document.querySelector('#repo-action-view'); + if (!el) return; + + // TODO: the parent element's full height doesn't work well now, + // but we can not pollute the global style at the moment, only fix the height problem for pages with this component + const parentFullHeight = document.querySelector<HTMLElement>('body > div.full.height'); + if (parentFullHeight) parentFullHeight.style.paddingBottom = '0'; + + const view = createApp(RepoActionView, { + runIndex: el.getAttribute('data-run-index'), + jobIndex: el.getAttribute('data-job-index'), + actionsURL: el.getAttribute('data-actions-url'), + locale: { + approve: el.getAttribute('data-locale-approve'), + cancel: el.getAttribute('data-locale-cancel'), + rerun: el.getAttribute('data-locale-rerun'), + rerun_all: el.getAttribute('data-locale-rerun-all'), + scheduled: el.getAttribute('data-locale-runs-scheduled'), + commit: el.getAttribute('data-locale-runs-commit'), + pushedBy: el.getAttribute('data-locale-runs-pushed-by'), + artifactsTitle: el.getAttribute('data-locale-artifacts-title'), + areYouSure: el.getAttribute('data-locale-are-you-sure'), + confirmDeleteArtifact: el.getAttribute('data-locale-confirm-delete-artifact'), + showTimeStamps: el.getAttribute('data-locale-show-timestamps'), + showLogSeconds: el.getAttribute('data-locale-show-log-seconds'), + showFullScreen: el.getAttribute('data-locale-show-full-screen'), + downloadLogs: el.getAttribute('data-locale-download-logs'), + status: { + unknown: el.getAttribute('data-locale-status-unknown'), + waiting: el.getAttribute('data-locale-status-waiting'), + running: el.getAttribute('data-locale-status-running'), + success: el.getAttribute('data-locale-status-success'), + failure: el.getAttribute('data-locale-status-failure'), + cancelled: el.getAttribute('data-locale-status-cancelled'), + skipped: el.getAttribute('data-locale-status-skipped'), + blocked: el.getAttribute('data-locale-status-blocked'), + }, + logsAlwaysAutoScroll: el.getAttribute('data-locale-logs-always-auto-scroll'), + logsAlwaysExpandRunning: el.getAttribute('data-locale-logs-always-expand-running'), + }, + }); + view.mount(el); +} diff --git a/web_src/js/features/repo-commit.ts b/web_src/js/features/repo-commit.ts index 56493443d9..8994a57f4a 100644 --- a/web_src/js/features/repo-commit.ts +++ b/web_src/js/features/repo-commit.ts @@ -2,7 +2,7 @@ import {createTippy} from '../modules/tippy.ts'; import {toggleElem} from '../utils/dom.ts'; export function initRepoEllipsisButton() { - for (const button of document.querySelectorAll('.js-toggle-commit-body')) { + for (const button of document.querySelectorAll<HTMLButtonElement>('.js-toggle-commit-body')) { button.addEventListener('click', function (e) { e.preventDefault(); const expanded = this.getAttribute('aria-expanded') === 'true'; diff --git a/web_src/js/features/repo-common.ts b/web_src/js/features/repo-common.ts index 90860720e4..fb76d8ed36 100644 --- a/web_src/js/features/repo-common.ts +++ b/web_src/js/features/repo-common.ts @@ -1,4 +1,4 @@ -import {queryElems} from '../utils/dom.ts'; +import {queryElems, type DOMEvent} from '../utils/dom.ts'; import {POST} from '../modules/fetch.ts'; import {showErrorToast} from '../modules/toast.ts'; import {sleep} from '../utils.ts'; @@ -7,10 +7,10 @@ import {createApp} from 'vue'; import {toOriginUrl} from '../utils/url.ts'; import {createTippy} from '../modules/tippy.ts'; -async function onDownloadArchive(e) { +async function onDownloadArchive(e: DOMEvent<MouseEvent>) { e.preventDefault(); // there are many places using the "archive-link", eg: the dropdown on the repo code page, the release list - const el = e.target.closest('a.archive-link[href]'); + const el = e.target.closest<HTMLAnchorElement>('a.archive-link[href]'); const targetLoading = el.closest('.ui.dropdown') ?? el; targetLoading.classList.add('is-loading', 'loading-icon-2px'); try { @@ -107,7 +107,7 @@ export function initRepoCloneButtons() { queryElems(document, '.clone-buttons-combo', initCloneSchemeUrlSelection); } -export async function updateIssuesMeta(url, action, issue_ids, id) { +export async function updateIssuesMeta(url: string, action: string, issue_ids: string, id: string) { try { const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})}); if (!response.ok) { diff --git a/web_src/js/features/repo-diff.ts b/web_src/js/features/repo-diff.ts index 0cb2e566c0..0dad4da862 100644 --- a/web_src/js/features/repo-diff.ts +++ b/web_src/js/features/repo-diff.ts @@ -168,7 +168,7 @@ function onShowMoreFiles() { initDiffHeaderPopup(); } -export async function loadMoreFiles(url) { +export async function loadMoreFiles(url: string) { const target = document.querySelector('a#diff-show-more-files'); if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) { return; diff --git a/web_src/js/features/repo-editor.ts b/web_src/js/features/repo-editor.ts index d7097787d2..0f3fb7bbcf 100644 --- a/web_src/js/features/repo-editor.ts +++ b/web_src/js/features/repo-editor.ts @@ -168,7 +168,7 @@ export function initRepoEditor() { silent: true, dirtyClass: dirtyFileClass, fieldSelector: ':input:not(.commit-form-wrapper :input)', - change($form) { + change($form: any) { const dirty = $form[0]?.classList.contains(dirtyFileClass); commitButton.disabled = !dirty; }, diff --git a/web_src/js/features/repo-findfile.ts b/web_src/js/features/repo-findfile.ts index 6500978bc8..59c827126f 100644 --- a/web_src/js/features/repo-findfile.ts +++ b/web_src/js/features/repo-findfile.ts @@ -4,13 +4,15 @@ import {pathEscapeSegments} from '../utils/url.ts'; import {GET} from '../modules/fetch.ts'; const threshold = 50; -let files = []; -let repoFindFileInput, repoFindFileTableBody, repoFindFileNoResult; +let files: Array<string> = []; +let repoFindFileInput: HTMLInputElement; +let repoFindFileTableBody: HTMLElement; +let repoFindFileNoResult: HTMLElement; // return the case-insensitive sub-match result as an array: [unmatched, matched, unmatched, matched, ...] // res[even] is unmatched, res[odd] is matched, see unit tests for examples // argument subLower must be a lower-cased string. -export function strSubMatch(full, subLower) { +export function strSubMatch(full: string, subLower: string) { const res = ['']; let i = 0, j = 0; const fullLower = full.toLowerCase(); @@ -38,7 +40,7 @@ export function strSubMatch(full, subLower) { return res; } -export function calcMatchedWeight(matchResult) { +export function calcMatchedWeight(matchResult: Array<any>) { let weight = 0; for (let i = 0; i < matchResult.length; i++) { if (i % 2 === 1) { // matches are on odd indices, see strSubMatch @@ -49,7 +51,7 @@ export function calcMatchedWeight(matchResult) { return weight; } -export function filterRepoFilesWeighted(files, filter) { +export function filterRepoFilesWeighted(files: Array<string>, filter: string) { let filterResult = []; if (filter) { const filterLower = filter.toLowerCase(); @@ -71,7 +73,7 @@ export function filterRepoFilesWeighted(files, filter) { return filterResult; } -function filterRepoFiles(filter) { +function filterRepoFiles(filter: string) { const treeLink = repoFindFileInput.getAttribute('data-url-tree-link'); repoFindFileTableBody.innerHTML = ''; diff --git a/web_src/js/features/repo-home.ts b/web_src/js/features/repo-home.ts index 9c1e317486..04a1288626 100644 --- a/web_src/js/features/repo-home.ts +++ b/web_src/js/features/repo-home.ts @@ -89,10 +89,10 @@ export function initRepoTopicBar() { url: `${appSubUrl}/explore/topics/search?q={query}`, throttle: 500, cache: false, - onResponse(res) { + onResponse(this: any, res: any) { const formattedResponse = { success: false, - results: [], + results: [] as Array<Record<string, any>>, }; const query = stripTags(this.urlData.query.trim()); let found_query = false; @@ -134,12 +134,12 @@ export function initRepoTopicBar() { return formattedResponse; }, }, - onLabelCreate(value) { + onLabelCreate(value: string) { value = value.toLowerCase().trim(); this.attr('data-value', value).contents().first().replaceWith(value); return fomanticQuery(this); }, - onAdd(addedValue, _addedText, $addedChoice) { + onAdd(addedValue: string, _addedText: any, $addedChoice: any) { addedValue = addedValue.toLowerCase().trim(); $addedChoice[0].setAttribute('data-value', addedValue); $addedChoice[0].setAttribute('data-text', addedValue); diff --git a/web_src/js/features/repo-issue-content.ts b/web_src/js/features/repo-issue-content.ts index 2279c26beb..056b810be8 100644 --- a/web_src/js/features/repo-issue-content.ts +++ b/web_src/js/features/repo-issue-content.ts @@ -33,7 +33,7 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo $fomanticDropdownOptions.dropdown({ showOnFocus: false, allowReselection: true, - async onChange(_value, _text, $item) { + async onChange(_value: string, _text: string, $item: any) { const optionItem = $item.data('option-item'); if (optionItem === 'delete') { if (window.confirm(i18nTextDeleteFromHistoryConfirm)) { @@ -115,7 +115,7 @@ function showContentHistoryMenu(issueBaseUrl: string, elCommentItem: Element, co onHide() { $fomanticDropdown.dropdown('change values', null); }, - onChange(value, itemHtml, $item) { + onChange(value: string, itemHtml: string, $item: any) { if (value && !$item.find('[data-history-is-deleted=1]').length) { showContentHistoryDetail(issueBaseUrl, commentId, value, itemHtml); } diff --git a/web_src/js/features/repo-issue-edit.ts b/web_src/js/features/repo-issue-edit.ts index 38dfea4743..a3a13a156d 100644 --- a/web_src/js/features/repo-issue-edit.ts +++ b/web_src/js/features/repo-issue-edit.ts @@ -2,14 +2,14 @@ import {handleReply} from './repo-issue.ts'; import {getComboMarkdownEditor, initComboMarkdownEditor, ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; import {POST} from '../modules/fetch.ts'; import {showErrorToast} from '../modules/toast.ts'; -import {hideElem, querySingleVisibleElem, showElem} from '../utils/dom.ts'; +import {hideElem, querySingleVisibleElem, showElem, type DOMEvent} from '../utils/dom.ts'; import {attachRefIssueContextPopup} from './contextpopup.ts'; import {initCommentContent, initMarkupContent} from '../markup/content.ts'; import {triggerUploadStateChanged} from './comp/EditorUpload.ts'; import {convertHtmlToMarkdown} from '../markup/html2markdown.ts'; import {applyAreYouSure, reinitializeAreYouSure} from '../vendor/jquery.are-you-sure.ts'; -async function tryOnEditContent(e) { +async function tryOnEditContent(e: DOMEvent<MouseEvent>) { const clickTarget = e.target.closest('.edit-content'); if (!clickTarget) return; @@ -21,14 +21,14 @@ async function tryOnEditContent(e) { let comboMarkdownEditor : ComboMarkdownEditor; - const cancelAndReset = (e) => { + const cancelAndReset = (e: Event) => { e.preventDefault(); showElem(renderContent); hideElem(editContentZone); comboMarkdownEditor.dropzoneReloadFiles(); }; - const saveAndRefresh = async (e) => { + const saveAndRefresh = async (e: Event) => { e.preventDefault(); // we are already in a form, do not bubble up to the document otherwise there will be other "form submit handlers" // at the moment, the form submit event conflicts with initRepoDiffConversationForm (global '.conversation-holder form' event handler) @@ -60,7 +60,7 @@ async function tryOnEditContent(e) { } else { renderContent.innerHTML = data.content; rawContent.textContent = comboMarkdownEditor.value(); - const refIssues = renderContent.querySelectorAll('p .ref-issue'); + const refIssues = renderContent.querySelectorAll<HTMLElement>('p .ref-issue'); attachRefIssueContextPopup(refIssues); } const content = segment; @@ -125,7 +125,7 @@ function extractSelectedMarkdown(container: HTMLElement) { return convertHtmlToMarkdown(el); } -async function tryOnQuoteReply(e) { +async function tryOnQuoteReply(e: Event) { const clickTarget = (e.target as HTMLElement).closest('.quote-reply'); if (!clickTarget) return; @@ -139,7 +139,7 @@ async function tryOnQuoteReply(e) { let editor; if (clickTarget.classList.contains('quote-reply-diff')) { - const replyBtn = clickTarget.closest('.comment-code-cloud').querySelector('button.comment-form-reply'); + const replyBtn = clickTarget.closest('.comment-code-cloud').querySelector<HTMLElement>('button.comment-form-reply'); editor = await handleReply(replyBtn); } else { // for normal issue/comment page diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts index 74d4362bfd..01d4bb6f78 100644 --- a/web_src/js/features/repo-issue-list.ts +++ b/web_src/js/features/repo-issue-list.ts @@ -7,6 +7,7 @@ import {createSortable} from '../modules/sortable.ts'; import {DELETE, POST} from '../modules/fetch.ts'; import {parseDom} from '../utils.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts'; +import type {SortableEvent} from 'sortablejs'; function initRepoIssueListCheckboxes() { const issueSelectAll = document.querySelector<HTMLInputElement>('.issue-checkbox-all'); @@ -104,7 +105,7 @@ function initDropdownUserRemoteSearch(el: Element) { $searchDropdown.dropdown('setting', { fullTextSearch: true, selectOnKeydown: false, - action: (_text, value) => { + action: (_text: string, value: string) => { window.location.href = actionJumpUrl.replace('{username}', encodeURIComponent(value)); }, }); @@ -133,7 +134,7 @@ function initDropdownUserRemoteSearch(el: Element) { $searchDropdown.dropdown('setting', 'apiSettings', { cache: false, url: `${searchUrl}&q={query}`, - onResponse(resp) { + onResponse(resp: any) { // the content is provided by backend IssuePosters handler processedResults.length = 0; for (const item of resp.results) { @@ -153,7 +154,7 @@ function initDropdownUserRemoteSearch(el: Element) { const dropdownSetup = {...$searchDropdown.dropdown('internal', 'setup')}; const dropdownTemplates = $searchDropdown.dropdown('setting', 'templates'); $searchDropdown.dropdown('internal', 'setup', dropdownSetup); - dropdownSetup.menu = function (values) { + dropdownSetup.menu = function (values: any) { // remove old dynamic items for (const el of elMenu.querySelectorAll(':scope > .dynamic-item')) { el.remove(); @@ -193,7 +194,7 @@ function initPinRemoveButton() { } } -async function pinMoveEnd(e) { +async function pinMoveEnd(e: SortableEvent) { const url = e.item.getAttribute('data-move-url'); const id = Number(e.item.getAttribute('data-issue-id')); await POST(url, {data: {id, position: e.newIndex + 1}}); diff --git a/web_src/js/features/repo-issue-sidebar-combolist.ts b/web_src/js/features/repo-issue-sidebar-combolist.ts index 24d620547f..8db2f7665f 100644 --- a/web_src/js/features/repo-issue-sidebar-combolist.ts +++ b/web_src/js/features/repo-issue-sidebar-combolist.ts @@ -46,7 +46,7 @@ class IssueSidebarComboList { return Array.from(this.elDropdown.querySelectorAll('.menu > .item.checked'), (el) => el.getAttribute('data-value')); } - updateUiList(changedValues) { + updateUiList(changedValues: Array<string>) { const elEmptyTip = this.elList.querySelector('.item.empty-list'); queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove()); for (const value of changedValues) { @@ -60,7 +60,7 @@ class IssueSidebarComboList { toggleElem(elEmptyTip, !hasItems); } - async updateToBackend(changedValues) { + async updateToBackend(changedValues: Array<string>) { if (this.updateAlgo === 'diff') { for (const value of this.initialValues) { if (!changedValues.includes(value)) { @@ -93,7 +93,7 @@ class IssueSidebarComboList { } } - async onItemClick(e) { + async onItemClick(e: Event) { const elItem = (e.target as HTMLElement).closest('.item'); if (!elItem) return; e.preventDefault(); diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts index d74d3f7700..a0cb875a87 100644 --- a/web_src/js/features/repo-issue.ts +++ b/web_src/js/features/repo-issue.ts @@ -32,8 +32,8 @@ export function initRepoIssueSidebarList() { fullTextSearch: true, apiSettings: { url: issueSearchUrl, - onResponse(response) { - const filteredResponse = {success: true, results: []}; + onResponse(response: any) { + const filteredResponse = {success: true, results: [] as Array<Record<string, any>>}; const currIssueId = $('#new-dependency-drop-list').data('issue-id'); // Parse the response from the api to work with our dropdown $.each(response, (_i, issue) => { @@ -216,7 +216,7 @@ export function initRepoIssueCodeCommentCancel() { export function initRepoPullRequestUpdate() { // Pull Request update button - const pullUpdateButton = document.querySelector('.update-button > button'); + const pullUpdateButton = document.querySelector<HTMLButtonElement>('.update-button > button'); if (!pullUpdateButton) return; pullUpdateButton.addEventListener('click', async function (e) { @@ -247,7 +247,7 @@ export function initRepoPullRequestUpdate() { }); $('.update-button > .dropdown').dropdown({ - onChange(_text, _value, $choice) { + onChange(_text: string, _value: string, $choice: any) { const choiceEl = $choice[0]; const url = choiceEl.getAttribute('data-do'); if (url) { @@ -298,8 +298,8 @@ export function initRepoIssueReferenceRepositorySearch() { .dropdown({ apiSettings: { url: `${appSubUrl}/repo/search?q={query}&limit=20`, - onResponse(response) { - const filteredResponse = {success: true, results: []}; + onResponse(response: any) { + const filteredResponse = {success: true, results: [] as Array<Record<string, any>>}; $.each(response.data, (_r, repo) => { filteredResponse.results.push({ name: htmlEscape(repo.repository.full_name), @@ -310,7 +310,7 @@ export function initRepoIssueReferenceRepositorySearch() { }, cache: false, }, - onChange(_value, _text, $choice) { + onChange(_value: string, _text: string, $choice: any) { const $form = $choice.closest('form'); if (!$form.length) return; @@ -360,7 +360,7 @@ export function initRepoIssueComments() { }); } -export async function handleReply(el) { +export async function handleReply(el: HTMLElement) { const form = el.closest('.comment-code-cloud').querySelector('.comment-form'); const textarea = form.querySelector('textarea'); @@ -379,7 +379,7 @@ export function initRepoPullRequestReview() { const groupID = commentDiv.closest('div[id^="code-comments-"]')?.getAttribute('id'); if (groupID && groupID.startsWith('code-comments-')) { const id = groupID.slice(14); - const ancestorDiffBox = commentDiv.closest('.diff-file-box'); + const ancestorDiffBox = commentDiv.closest<HTMLElement>('.diff-file-box'); hideElem(`#show-outdated-${id}`); showElem(`#code-comments-${id}, #code-preview-${id}, #hide-outdated-${id}`); @@ -589,7 +589,7 @@ export function initRepoIssueBranchSelect() { }); } -async function initSingleCommentEditor($commentForm) { +async function initSingleCommentEditor($commentForm: any) { // pages: // * normal new issue/pr page: no status-button, no comment-button (there is only a normal submit button which can submit empty content) // * issue/pr view page: with comment form, has status-button and comment-button @@ -611,7 +611,7 @@ async function initSingleCommentEditor($commentForm) { syncUiState(); } -function initIssueTemplateCommentEditors($commentForm) { +function initIssueTemplateCommentEditors($commentForm: any) { // pages: // * new issue with issue template const $comboFields = $commentForm.find('.combo-editor-dropzone'); diff --git a/web_src/js/features/repo-migrate.ts b/web_src/js/features/repo-migrate.ts index b75289feec..0788f83215 100644 --- a/web_src/js/features/repo-migrate.ts +++ b/web_src/js/features/repo-migrate.ts @@ -1,11 +1,11 @@ -import {hideElem, showElem} from '../utils/dom.ts'; +import {hideElem, showElem, type DOMEvent} from '../utils/dom.ts'; import {GET, POST} from '../modules/fetch.ts'; export function initRepoMigrationStatusChecker() { const repoMigrating = document.querySelector('#repo_migrating'); if (!repoMigrating) return; - document.querySelector('#repo_migrating_retry')?.addEventListener('click', doMigrationRetry); + document.querySelector<HTMLButtonElement>('#repo_migrating_retry')?.addEventListener('click', doMigrationRetry); const repoLink = repoMigrating.getAttribute('data-migrating-repo-link'); @@ -55,7 +55,7 @@ export function initRepoMigrationStatusChecker() { syncTaskStatus(); // no await } -async function doMigrationRetry(e) { +async function doMigrationRetry(e: DOMEvent<MouseEvent>) { await POST(e.target.getAttribute('data-migrating-task-retry-url')); window.location.reload(); } diff --git a/web_src/js/features/repo-new.ts b/web_src/js/features/repo-new.ts index 8a77a77b4a..f2c5eba62c 100644 --- a/web_src/js/features/repo-new.ts +++ b/web_src/js/features/repo-new.ts @@ -23,7 +23,7 @@ function initRepoNewTemplateSearch(form: HTMLFormElement) { $dropdown.dropdown('setting', { apiSettings: { url: `${appSubUrl}/repo/search?q={query}&template=true&priority_owner_id=${inputRepoOwnerUid.value}`, - onResponse(response) { + onResponse(response: any) { const results = []; results.push({name: '', value: ''}); // empty item means not using template for (const tmplRepo of response.data) { @@ -66,7 +66,7 @@ export function initRepoNew() { let help = form.querySelector(`.help[data-help-for-repo-name="${CSS.escape(inputRepoName.value)}"]`); if (!help) help = form.querySelector(`.help[data-help-for-repo-name=""]`); showElem(help); - const repoNamePreferPrivate = {'.profile': false, '.profile-private': true}; + const repoNamePreferPrivate: Record<string, boolean> = {'.profile': false, '.profile-private': true}; const preferPrivate = repoNamePreferPrivate[inputRepoName.value]; // inputPrivate might be disabled because site admin "force private" if (preferPrivate !== undefined && !inputPrivate.closest('.disabled, [disabled]')) { diff --git a/web_src/js/features/repo-settings.ts b/web_src/js/features/repo-settings.ts index 90b0219f3e..b61ef9a153 100644 --- a/web_src/js/features/repo-settings.ts +++ b/web_src/js/features/repo-settings.ts @@ -12,7 +12,7 @@ function initRepoSettingsCollaboration() { for (const dropdownEl of queryElems(document, '.page-content.repository .ui.dropdown.access-mode')) { const textEl = dropdownEl.querySelector(':scope > .text'); $(dropdownEl).dropdown({ - async action(text, value) { + async action(text: string, value: string) { dropdownEl.classList.add('is-loading', 'loading-icon-2px'); const lastValue = dropdownEl.getAttribute('data-last-value'); $(dropdownEl).dropdown('hide'); @@ -53,8 +53,8 @@ function initRepoSettingsSearchTeamBox() { apiSettings: { url: `${appSubUrl}/org/${searchTeamBox.getAttribute('data-org-name')}/teams/-/search?q={query}`, headers: {'X-Csrf-Token': csrfToken}, - onResponse(response) { - const items = []; + onResponse(response: any) { + const items: Array<Record<string, any>> = []; $.each(response.data, (_i, item) => { items.push({ title: item.name, @@ -79,21 +79,21 @@ function initRepoSettingsGitHook() { function initRepoSettingsBranches() { if (!document.querySelector('.repository.settings.branches')) return; - for (const el of document.querySelectorAll('.toggle-target-enabled')) { + for (const el of document.querySelectorAll<HTMLInputElement>('.toggle-target-enabled')) { el.addEventListener('change', function () { const target = document.querySelector(this.getAttribute('data-target')); target?.classList.toggle('disabled', !this.checked); }); } - for (const el of document.querySelectorAll('.toggle-target-disabled')) { + for (const el of document.querySelectorAll<HTMLInputElement>('.toggle-target-disabled')) { el.addEventListener('change', function () { const target = document.querySelector(this.getAttribute('data-target')); if (this.checked) target?.classList.add('disabled'); // only disable, do not auto enable }); } - document.querySelector('#dismiss_stale_approvals')?.addEventListener('change', function () { + document.querySelector<HTMLInputElement>('#dismiss_stale_approvals')?.addEventListener('change', function () { document.querySelector('#ignore_stale_approvals_box')?.classList.toggle('disabled', this.checked); }); diff --git a/web_src/js/features/repo-wiki.ts b/web_src/js/features/repo-wiki.ts index 484c628f9f..9ffa8a3275 100644 --- a/web_src/js/features/repo-wiki.ts +++ b/web_src/js/features/repo-wiki.ts @@ -70,7 +70,7 @@ async function initRepoWikiFormEditor() { }); } -function collapseWikiTocForMobile(collapse) { +function collapseWikiTocForMobile(collapse: boolean) { if (collapse) { document.querySelector('.wiki-content-toc details')?.removeAttribute('open'); } diff --git a/web_src/js/features/sshkey-helper.ts b/web_src/js/features/sshkey-helper.ts index 9234e3ec44..860bc5b294 100644 --- a/web_src/js/features/sshkey-helper.ts +++ b/web_src/js/features/sshkey-helper.ts @@ -1,6 +1,6 @@ export function initSshKeyFormParser() { // Parse SSH Key - document.querySelector('#ssh-key-content')?.addEventListener('input', function () { + document.querySelector<HTMLTextAreaElement>('#ssh-key-content')?.addEventListener('input', function () { const arrays = this.value.split(' '); const title = document.querySelector<HTMLInputElement>('#ssh-key-title'); if (!title.value && arrays.length === 3 && arrays[2] !== '') { diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts index af52be4e24..a5cd5ae7c4 100644 --- a/web_src/js/features/stopwatch.ts +++ b/web_src/js/features/stopwatch.ts @@ -1,6 +1,6 @@ import {createTippy} from '../modules/tippy.ts'; import {GET} from '../modules/fetch.ts'; -import {hideElem, showElem} from '../utils/dom.ts'; +import {hideElem, queryElems, showElem} from '../utils/dom.ts'; import {logoutFromWorker} from '../modules/worker.ts'; const {appSubUrl, notificationSettings, enableTimeTracking, assetVersionEncoded} = window.config; @@ -38,7 +38,7 @@ export function initStopwatch() { } let usingPeriodicPoller = false; - const startPeriodicPoller = (timeout) => { + const startPeriodicPoller = (timeout: number) => { if (timeout <= 0 || !Number.isFinite(timeout)) return; usingPeriodicPoller = true; setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout); @@ -103,7 +103,7 @@ export function initStopwatch() { startPeriodicPoller(notificationSettings.MinTimeout); } -async function updateStopwatchWithCallback(callback, timeout) { +async function updateStopwatchWithCallback(callback: (timeout: number) => void, timeout: number) { const isSet = await updateStopwatch(); if (!isSet) { @@ -125,7 +125,7 @@ async function updateStopwatch() { return updateStopwatchData(data); } -function updateStopwatchData(data) { +function updateStopwatchData(data: any) { const watch = data[0]; const btnEls = document.querySelectorAll('.active-stopwatch'); if (!watch) { @@ -144,23 +144,10 @@ function updateStopwatchData(data) { return Boolean(data.length); } -// TODO: This flickers on page load, we could avoid this by making a custom -// element to render time periods. Feeding a datetime in backend does not work -// when time zone between server and client differs. -function updateStopwatchTime(seconds) { - if (!Number.isFinite(seconds)) return; - const datetime = (new Date(Date.now() - seconds * 1000)).toISOString(); - for (const parent of document.querySelectorAll('.header-stopwatch-dot')) { - const existing = parent.querySelector(':scope > relative-time'); - if (existing) { - existing.setAttribute('datetime', datetime); - } else { - const el = document.createElement('relative-time'); - el.setAttribute('format', 'micro'); - el.setAttribute('datetime', datetime); - el.setAttribute('lang', 'en-US'); - el.setAttribute('title', ''); // make <relative-time> show no title and therefor no tooltip - parent.append(el); - } - } +// TODO: This flickers on page load, we could avoid this by making a custom element to render time periods. +function updateStopwatchTime(seconds: number) { + const hours = seconds / 3600 || 0; + const minutes = seconds / 60 || 0; + const timeText = hours >= 1 ? `${Math.round(hours)}h` : `${Math.round(minutes)}m`; + queryElems(document, '.header-stopwatch-dot', (el) => el.textContent = timeText); } diff --git a/web_src/js/features/tablesort.ts b/web_src/js/features/tablesort.ts index 15ea358fa3..0648ffd067 100644 --- a/web_src/js/features/tablesort.ts +++ b/web_src/js/features/tablesort.ts @@ -9,7 +9,7 @@ export function initTableSort() { } } -function tableSort(normSort, revSort, isDefault) { +function tableSort(normSort: string, revSort: string, isDefault: string) { if (!normSort) return false; if (!revSort) revSort = ''; diff --git a/web_src/js/features/tribute.ts b/web_src/js/features/tribute.ts index fa65bcbb28..de1c3e97cd 100644 --- a/web_src/js/features/tribute.ts +++ b/web_src/js/features/tribute.ts @@ -1,14 +1,16 @@ import {emojiKeys, emojiHTML, emojiString} from './emoji.ts'; import {htmlEscape} from 'escape-goat'; -function makeCollections({mentions, emoji}) { - const collections = []; +type TributeItem = Record<string, any>; - if (emoji) { - collections.push({ +export async function attachTribute(element: HTMLElement) { + const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs'); + + const collections = [ + { // emojis trigger: ':', requireLeadingSpace: true, - values: (query, cb) => { + values: (query: string, cb: (matches: Array<string>) => void) => { const matches = []; for (const name of emojiKeys) { if (name.includes(query)) { @@ -18,22 +20,18 @@ function makeCollections({mentions, emoji}) { } cb(matches); }, - lookup: (item) => item, - selectTemplate: (item) => { + lookup: (item: TributeItem) => item, + selectTemplate: (item: TributeItem) => { if (item === undefined) return null; return emojiString(item.original); }, - menuItemTemplate: (item) => { + menuItemTemplate: (item: TributeItem) => { return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`; }, - }); - } - - if (mentions) { - collections.push({ + }, { // mentions values: window.config.mentionValues ?? [], requireLeadingSpace: true, - menuItemTemplate: (item) => { + menuItemTemplate: (item: TributeItem) => { return ` <div class="tribute-item"> <img src="${htmlEscape(item.original.avatar)}" width="21" height="21"/> @@ -42,15 +40,9 @@ function makeCollections({mentions, emoji}) { </div> `; }, - }); - } + }, + ]; - return collections; -} - -export async function attachTribute(element, {mentions, emoji}) { - const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs'); - const collections = makeCollections({mentions, emoji}); // @ts-expect-error TS2351: This expression is not constructable (strange, why) const tribute = new Tribute({collection: collections, noMatchTemplate: ''}); tribute.attach(element); diff --git a/web_src/js/features/user-auth-webauthn.ts b/web_src/js/features/user-auth-webauthn.ts index 70516c280d..b9ab2e2088 100644 --- a/web_src/js/features/user-auth-webauthn.ts +++ b/web_src/js/features/user-auth-webauthn.ts @@ -114,7 +114,7 @@ async function login2FA() { } } -async function verifyAssertion(assertedCredential) { +async function verifyAssertion(assertedCredential: any) { // TODO: Credential type does not work // Move data into Arrays in case it is super long const authData = new Uint8Array(assertedCredential.response.authenticatorData); const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON); @@ -148,7 +148,7 @@ async function verifyAssertion(assertedCredential) { window.location.href = reply?.redirect ?? `${appSubUrl}/`; } -async function webauthnRegistered(newCredential) { +async function webauthnRegistered(newCredential: any) { // TODO: Credential type does not work const attestationObject = new Uint8Array(newCredential.response.attestationObject); const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); const rawId = new Uint8Array(newCredential.rawId); diff --git a/web_src/js/features/user-settings.ts b/web_src/js/features/user-settings.ts index c097df7b6c..6312a8b682 100644 --- a/web_src/js/features/user-settings.ts +++ b/web_src/js/features/user-settings.ts @@ -13,7 +13,7 @@ export function initUserSettings() { initUserSettingsAvatarCropper(); - const usernameInput = document.querySelector('#username'); + const usernameInput = document.querySelector<HTMLInputElement>('#username'); if (!usernameInput) return; usernameInput.addEventListener('input', function () { const prompt = document.querySelector('#name-change-prompt'); diff --git a/web_src/js/index.ts b/web_src/js/index.ts index 022be033da..b89e596047 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -2,8 +2,7 @@ import './bootstrap.ts'; import './htmx.ts'; -import {initDashboardRepoList} from './components/DashboardRepoList.vue'; - +import {initDashboardRepoList} from './features/dashboard.ts'; import {initGlobalCopyToClipboardListener} from './features/clipboard.ts'; import {initContextPopups} from './features/contextpopup.ts'; import {initRepoGraphGit} from './features/repo-graph.ts'; @@ -53,7 +52,7 @@ import {initRepoWikiForm} from './features/repo-wiki.ts'; import {initRepository, initBranchSelectorTabs} from './features/repo-legacy.ts'; import {initCopyContent} from './features/copycontent.ts'; import {initCaptcha} from './features/captcha.ts'; -import {initRepositoryActionView} from './components/RepoActionView.vue'; +import {initRepositoryActionView} from './features/repo-actions.ts'; import {initGlobalTooltips} from './modules/tippy.ts'; import {initGiteaFomantic} from './modules/fomantic.ts'; import {initSubmitEventPolyfill, onDomReady} from './utils/dom.ts'; diff --git a/web_src/js/markup/asciicast.ts b/web_src/js/markup/asciicast.ts index 97b18743a1..9baae6ba85 100644 --- a/web_src/js/markup/asciicast.ts +++ b/web_src/js/markup/asciicast.ts @@ -3,6 +3,7 @@ export async function renderAsciicast() { if (!els.length) return; const [player] = await Promise.all([ + // @ts-expect-error: module exports no types import(/* webpackChunkName: "asciinema-player" */'asciinema-player'), import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), ]); diff --git a/web_src/js/markup/html2markdown.ts b/web_src/js/markup/html2markdown.ts index fc2083e86d..8c2d2f8c86 100644 --- a/web_src/js/markup/html2markdown.ts +++ b/web_src/js/markup/html2markdown.ts @@ -1,7 +1,9 @@ import {htmlEscape} from 'escape-goat'; +type Processor = (el: HTMLElement) => string | HTMLElement | void; + type Processors = { - [tagName: string]: (el: HTMLElement) => string | HTMLElement | void; + [tagName: string]: Processor; } type ProcessorContext = { @@ -11,7 +13,7 @@ type ProcessorContext = { } function prepareProcessors(ctx:ProcessorContext): Processors { - const processors = { + const processors: Processors = { H1(el: HTMLElement) { const level = parseInt(el.tagName.slice(1)); el.textContent = `${'#'.repeat(level)} ${el.textContent.trim()}`; diff --git a/web_src/js/modules/fomantic/dimmer.ts b/web_src/js/modules/fomantic/dimmer.ts index 4e05cac0cd..cbdfac23cb 100644 --- a/web_src/js/modules/fomantic/dimmer.ts +++ b/web_src/js/modules/fomantic/dimmer.ts @@ -3,7 +3,7 @@ import {queryElemChildren} from '../../utils/dom.ts'; export function initFomanticDimmer() { // stand-in for removed dimmer module - $.fn.dimmer = function (arg0: string, arg1: any) { + $.fn.dimmer = function (this: any, arg0: string, arg1: any) { if (arg0 === 'add content') { const $el = arg1; const existingDimmer = document.querySelector('body > .ui.dimmer'); diff --git a/web_src/js/modules/fomantic/dropdown.ts b/web_src/js/modules/fomantic/dropdown.ts index 9bdc9bfc33..8736e041df 100644 --- a/web_src/js/modules/fomantic/dropdown.ts +++ b/web_src/js/modules/fomantic/dropdown.ts @@ -17,7 +17,7 @@ export function initAriaDropdownPatch() { // the patched `$.fn.dropdown` function, it passes the arguments to Fomantic's `$.fn.dropdown` function, and: // * it does the one-time attaching on the first call // * it delegates the `onLabelCreate` to the patched `onLabelCreate` to add necessary aria attributes -function ariaDropdownFn(...args: Parameters<FomanticInitFunction>) { +function ariaDropdownFn(this: any, ...args: Parameters<FomanticInitFunction>) { const ret = fomanticDropdownFn.apply(this, args); // if the `$().dropdown()` call is without arguments, or it has non-string (object) argument, @@ -38,7 +38,7 @@ function ariaDropdownFn(...args: Parameters<FomanticInitFunction>) { // the elements inside the dropdown menu item should not be focusable, the focus should always be on the dropdown primary element. function updateMenuItem(dropdown: HTMLElement, item: HTMLElement) { if (!item.id) item.id = generateAriaId(); - item.setAttribute('role', dropdown[ariaPatchKey].listItemRole); + item.setAttribute('role', (dropdown as any)[ariaPatchKey].listItemRole); item.setAttribute('tabindex', '-1'); for (const el of item.querySelectorAll('a, input, button')) el.setAttribute('tabindex', '-1'); } @@ -61,7 +61,7 @@ function updateSelectionLabel(label: HTMLElement) { } } -function processMenuItems($dropdown, dropdownCall) { +function processMenuItems($dropdown: any, dropdownCall: any) { const hideEmptyDividers = dropdownCall('setting', 'hideDividers') === 'empty'; const itemsMenu = $dropdown[0].querySelector('.scrolling.menu') || $dropdown[0].querySelector('.menu'); if (hideEmptyDividers) hideScopedEmptyDividers(itemsMenu); @@ -76,18 +76,18 @@ function delegateOne($dropdown: any) { const oldFocusSearch = dropdownCall('internal', 'focusSearch'); const oldBlurSearch = dropdownCall('internal', 'blurSearch'); // * If the "dropdown icon" is clicked, Fomantic calls "focusSearch", so show the menu - dropdownCall('internal', 'focusSearch', function () { dropdownCall('show'); oldFocusSearch.call(this) }); + dropdownCall('internal', 'focusSearch', function (this: any) { dropdownCall('show'); oldFocusSearch.call(this) }); // * If the "dropdown icon" is clicked again when the menu is visible, Fomantic calls "blurSearch", so hide the menu - dropdownCall('internal', 'blurSearch', function () { oldBlurSearch.call(this); dropdownCall('hide') }); + dropdownCall('internal', 'blurSearch', function (this: any) { oldBlurSearch.call(this); dropdownCall('hide') }); const oldFilterItems = dropdownCall('internal', 'filterItems'); - dropdownCall('internal', 'filterItems', function (...args: any[]) { + dropdownCall('internal', 'filterItems', function (this: any, ...args: any[]) { oldFilterItems.call(this, ...args); processMenuItems($dropdown, dropdownCall); }); const oldShow = dropdownCall('internal', 'show'); - dropdownCall('internal', 'show', function (...args: any[]) { + dropdownCall('internal', 'show', function (this: any, ...args: any[]) { oldShow.call(this, ...args); processMenuItems($dropdown, dropdownCall); }); @@ -110,7 +110,7 @@ function delegateOne($dropdown: any) { // the `onLabelCreate` is used to add necessary aria attributes for dynamically created selection labels const dropdownOnLabelCreateOld = dropdownCall('setting', 'onLabelCreate'); - dropdownCall('setting', 'onLabelCreate', function(value: any, text: string) { + dropdownCall('setting', 'onLabelCreate', function(this: any, value: any, text: string) { const $label = dropdownOnLabelCreateOld.call(this, value, text); updateSelectionLabel($label[0]); return $label; @@ -143,7 +143,7 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men $(menu).find('> .item').each((_, item) => updateMenuItem(dropdown, item)); // this role could only be changed after its content is ready, otherwise some browsers+readers (like Chrome+AppleVoice) crash - menu.setAttribute('role', dropdown[ariaPatchKey].listPopupRole); + menu.setAttribute('role', (dropdown as any)[ariaPatchKey].listPopupRole); // prepare selection label items for (const label of dropdown.querySelectorAll<HTMLElement>('.ui.label')) { @@ -151,8 +151,8 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men } // make the primary element (focusable) aria-friendly - focusable.setAttribute('role', focusable.getAttribute('role') ?? dropdown[ariaPatchKey].focusableRole); - focusable.setAttribute('aria-haspopup', dropdown[ariaPatchKey].listPopupRole); + focusable.setAttribute('role', focusable.getAttribute('role') ?? (dropdown as any)[ariaPatchKey].focusableRole); + focusable.setAttribute('aria-haspopup', (dropdown as any)[ariaPatchKey].listPopupRole); focusable.setAttribute('aria-controls', menu.id); focusable.setAttribute('aria-expanded', 'false'); @@ -164,7 +164,7 @@ function attachStaticElements(dropdown: HTMLElement, focusable: HTMLElement, men } function attachInit(dropdown: HTMLElement) { - dropdown[ariaPatchKey] = {}; + (dropdown as any)[ariaPatchKey] = {}; if (dropdown.classList.contains('custom')) return; // Dropdown has 2 different focusing behaviors @@ -204,9 +204,9 @@ function attachInit(dropdown: HTMLElement) { // Since #19861 we have prepared the "combobox" solution, but didn't get enough time to put it into practice and test before. const isComboBox = dropdown.querySelectorAll('input').length > 0; - dropdown[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'menu'; - dropdown[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : ''; - dropdown[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem'; + (dropdown as any)[ariaPatchKey].focusableRole = isComboBox ? 'combobox' : 'menu'; + (dropdown as any)[ariaPatchKey].listPopupRole = isComboBox ? 'listbox' : ''; + (dropdown as any)[ariaPatchKey].listItemRole = isComboBox ? 'option' : 'menuitem'; attachDomEvents(dropdown, focusable, menu); attachStaticElements(dropdown, focusable, menu); @@ -229,7 +229,7 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT // if the popup is visible and has an active/selected item, use its id as aria-activedescendant if (menuVisible) { focusable.setAttribute('aria-activedescendant', active.id); - } else if (dropdown[ariaPatchKey].listPopupRole === 'menu') { + } else if ((dropdown as any)[ariaPatchKey].listPopupRole === 'menu') { // for menu, when the popup is hidden, no need to keep the aria-activedescendant, and clear the active/selected item focusable.removeAttribute('aria-activedescendant'); active.classList.remove('active', 'selected'); @@ -253,7 +253,7 @@ function attachDomEvents(dropdown: HTMLElement, focusable: HTMLElement, menu: HT // when the popup is hiding, it's better to have a small "delay", because there is a Fomantic UI animation // without the delay for hiding, the UI will be somewhat laggy and sometimes may get stuck in the animation. const deferredRefreshAriaActiveItem = (delay = 0) => { setTimeout(refreshAriaActiveItem, delay) }; - dropdown[ariaPatchKey].deferredRefreshAriaActiveItem = deferredRefreshAriaActiveItem; + (dropdown as any)[ariaPatchKey].deferredRefreshAriaActiveItem = deferredRefreshAriaActiveItem; dropdown.addEventListener('keyup', (e) => { if (e.key.startsWith('Arrow')) deferredRefreshAriaActiveItem(); }); // if the dropdown has been opened by focus, do not trigger the next click event again. @@ -363,7 +363,7 @@ function onResponseKeepSelectedItem(dropdown: typeof $|HTMLElement, selectedValu // then the dropdown only shows other items and will select another (wrong) one. // It can't be easily fix by using setTimeout(patch, 0) in `onResponse` because the `onResponse` is called before another `setTimeout(..., timeLeft)` // Fortunately, the "timeLeft" is controlled by "loadingDuration" which is always zero at the moment, so we can use `setTimeout(..., 10)` - const elDropdown = (dropdown instanceof HTMLElement) ? dropdown : dropdown[0]; + const elDropdown = (dropdown instanceof HTMLElement) ? dropdown : (dropdown as any)[0]; setTimeout(() => { queryElems(elDropdown, `.menu .item[data-value="${CSS.escape(selectedValue)}"].filtered`, (el) => el.classList.remove('filtered')); $(elDropdown).dropdown('set selected', selectedValue ?? ''); diff --git a/web_src/js/modules/fomantic/modal.ts b/web_src/js/modules/fomantic/modal.ts index fb80047d01..6a2c558890 100644 --- a/web_src/js/modules/fomantic/modal.ts +++ b/web_src/js/modules/fomantic/modal.ts @@ -12,7 +12,7 @@ export function initAriaModalPatch() { // the patched `$.fn.modal` modal function // * it does the one-time attaching on the first call -function ariaModalFn(...args: Parameters<FomanticInitFunction>) { +function ariaModalFn(this: any, ...args: Parameters<FomanticInitFunction>) { const ret = fomanticModalFn.apply(this, args); if (args[0] === 'show' || args[0]?.autoShow) { for (const el of this) { diff --git a/web_src/js/modules/tippy.ts b/web_src/js/modules/tippy.ts index aaaf580de1..bc6d5bfdd6 100644 --- a/web_src/js/modules/tippy.ts +++ b/web_src/js/modules/tippy.ts @@ -121,7 +121,7 @@ function switchTitleToTooltip(target: Element): void { * Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)" * The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy */ -function lazyTooltipOnMouseHover(e: Event): void { +function lazyTooltipOnMouseHover(this: HTMLElement, e: Event): void { e.target.removeEventListener('mouseover', lazyTooltipOnMouseHover, true); attachTooltip(this); } diff --git a/web_src/js/standalone/devtest.ts b/web_src/js/standalone/devtest.ts index 3489697a2f..e6baf6c9ce 100644 --- a/web_src/js/standalone/devtest.ts +++ b/web_src/js/standalone/devtest.ts @@ -1,7 +1,7 @@ import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.ts'; function initDevtestToast() { - const levelMap = {info: showInfoToast, warning: showWarningToast, error: showErrorToast}; + const levelMap: Record<string, any> = {info: showInfoToast, warning: showWarningToast, error: showErrorToast}; for (const el of document.querySelectorAll('.toast-test-button')) { el.addEventListener('click', () => { const level = el.getAttribute('data-toast-level'); diff --git a/web_src/js/svg.ts b/web_src/js/svg.ts index 6a8246fa1b..b193afb255 100644 --- a/web_src/js/svg.ts +++ b/web_src/js/svg.ts @@ -1,4 +1,4 @@ -import {h} from 'vue'; +import {defineComponent, h, type PropType} from 'vue'; import {parseDom, serializeXml} from './utils.ts'; import giteaDoubleChevronLeft from '../../public/assets/img/svg/gitea-double-chevron-left.svg'; import giteaDoubleChevronRight from '../../public/assets/img/svg/gitea-double-chevron-right.svg'; @@ -35,6 +35,7 @@ import octiconGitBranch from '../../public/assets/img/svg/octicon-git-branch.svg import octiconGitCommit from '../../public/assets/img/svg/octicon-git-commit.svg'; import octiconGitMerge from '../../public/assets/img/svg/octicon-git-merge.svg'; import octiconGitPullRequest from '../../public/assets/img/svg/octicon-git-pull-request.svg'; +import octiconGitPullRequestClosed from '../../public/assets/img/svg/octicon-git-pull-request-closed.svg'; import octiconGitPullRequestDraft from '../../public/assets/img/svg/octicon-git-pull-request-draft.svg'; import octiconGrabber from '../../public/assets/img/svg/octicon-grabber.svg'; import octiconHeading from '../../public/assets/img/svg/octicon-heading.svg'; @@ -112,6 +113,7 @@ const svgs = { 'octicon-git-commit': octiconGitCommit, 'octicon-git-merge': octiconGitMerge, 'octicon-git-pull-request': octiconGitPullRequest, + 'octicon-git-pull-request-closed': octiconGitPullRequestClosed, 'octicon-git-pull-request-draft': octiconGitPullRequestDraft, 'octicon-grabber': octiconGrabber, 'octicon-heading': octiconHeading, @@ -194,10 +196,10 @@ export function svgParseOuterInner(name: SvgName) { return {svgOuter, svgInnerHtml}; } -export const SvgIcon = { +export const SvgIcon = defineComponent({ name: 'SvgIcon', props: { - name: {type: String, required: true}, + name: {type: String as PropType<SvgName>, required: true}, size: {type: Number, default: 16}, className: {type: String, default: ''}, symbolId: {type: String}, @@ -206,7 +208,7 @@ export const SvgIcon = { let {svgOuter, svgInnerHtml} = svgParseOuterInner(this.name); // https://vuejs.org/guide/extras/render-function.html#creating-vnodes // the `^` is used for attr, set SVG attributes like 'width', `aria-hidden`, `viewBox`, etc - const attrs = {}; + const attrs: Record<string, any> = {}; for (const attr of svgOuter.attributes) { if (attr.name === 'class') continue; attrs[`^${attr.name}`] = attr.value; @@ -215,7 +217,7 @@ export const SvgIcon = { attrs[`^height`] = this.size; // make the <SvgIcon class="foo" class-name="bar"> classes work together - const classes = []; + const classes: Array<string> = []; for (const cls of svgOuter.classList) { classes.push(cls); } @@ -234,4 +236,4 @@ export const SvgIcon = { innerHTML: svgInnerHtml, }); }, -}; +}); diff --git a/web_src/js/types.ts b/web_src/js/types.ts index e7c9ac0df4..1b5e652f66 100644 --- a/web_src/js/types.ts +++ b/web_src/js/types.ts @@ -22,6 +22,8 @@ export type Config = { i18n: Record<string, string>, } +export type IntervalId = ReturnType<typeof setInterval>; + export type Intent = 'error' | 'warning' | 'info'; export type RequestData = string | FormData | URLSearchParams | Record<string, any>; @@ -30,6 +32,11 @@ export type RequestOpts = { data?: RequestData, } & RequestInit; +export type RepoOwnerPathInfo = { + ownerName: string, + repoName: string, +} + export type IssuePathInfo = { ownerName: string, repoName: string, diff --git a/web_src/js/utils.test.ts b/web_src/js/utils.test.ts index b527111533..ccdbc2dbd7 100644 --- a/web_src/js/utils.test.ts +++ b/web_src/js/utils.test.ts @@ -1,7 +1,7 @@ import { basename, extname, isObject, stripTags, parseIssueHref, parseUrl, translateMonth, translateDay, blobToDataURI, - toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, parseIssueNewHref, + toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, isImageFile, isVideoFile, parseRepoOwnerPathInfo, } from './utils.ts'; test('basename', () => { @@ -45,12 +45,14 @@ test('parseIssueHref', () => { expect(parseIssueHref('')).toEqual({ownerName: undefined, repoName: undefined, type: undefined, index: undefined}); }); -test('parseIssueNewHref', () => { - expect(parseIssueNewHref('/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'}); - expect(parseIssueNewHref('/owner/repo/issues/new?query')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'}); - expect(parseIssueNewHref('/sub/owner/repo/issues/new#hash')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'issues'}); - expect(parseIssueNewHref('/sub/owner/repo/compare/feature/branch-1...fix/branch-2')).toEqual({ownerName: 'owner', repoName: 'repo', pathType: 'pulls'}); - expect(parseIssueNewHref('/other')).toEqual({}); +test('parseRepoOwnerPathInfo', () => { + expect(parseRepoOwnerPathInfo('/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo'}); + expect(parseRepoOwnerPathInfo('/owner/repo/releases')).toEqual({ownerName: 'owner', repoName: 'repo'}); + expect(parseRepoOwnerPathInfo('/other')).toEqual({}); + window.config.appSubUrl = '/sub'; + expect(parseRepoOwnerPathInfo('/sub/owner/repo/issues/new')).toEqual({ownerName: 'owner', repoName: 'repo'}); + expect(parseRepoOwnerPathInfo('/sub/owner/repo/compare/feature/branch-1...fix/branch-2')).toEqual({ownerName: 'owner', repoName: 'repo'}); + window.config.appSubUrl = ''; }); test('parseUrl', () => { diff --git a/web_src/js/utils.ts b/web_src/js/utils.ts index 2a2bdc60f9..54f59a2c03 100644 --- a/web_src/js/utils.ts +++ b/web_src/js/utils.ts @@ -1,5 +1,5 @@ import {decode, encode} from 'uint8-to-base64'; -import type {IssuePageInfo, IssuePathInfo} from './types.ts'; +import type {IssuePageInfo, IssuePathInfo, RepoOwnerPathInfo} from './types.ts'; // transform /path/to/file.ext to file.ext export function basename(path: string): string { @@ -32,16 +32,17 @@ export function stripTags(text: string): string { } export function parseIssueHref(href: string): IssuePathInfo { + // FIXME: it should use pathname and trim the appSubUrl ahead const path = (href || '').replace(/[#?].*$/, ''); const [_, ownerName, repoName, pathType, indexString] = /([^/]+)\/([^/]+)\/(issues|pulls)\/([0-9]+)/.exec(path) || []; return {ownerName, repoName, pathType, indexString}; } -export function parseIssueNewHref(href: string): IssuePathInfo { - const path = (href || '').replace(/[#?].*$/, ''); - const [_, ownerName, repoName, pathTypeField] = /([^/]+)\/([^/]+)\/(issues\/new|compare\/.+\.\.\.)/.exec(path) || []; - const pathType = pathTypeField ? (pathTypeField.startsWith('issues/new') ? 'issues' : 'pulls') : undefined; - return {ownerName, repoName, pathType}; +export function parseRepoOwnerPathInfo(pathname: string): RepoOwnerPathInfo { + const appSubUrl = window.config.appSubUrl; + if (appSubUrl && pathname.startsWith(appSubUrl)) pathname = pathname.substring(appSubUrl.length); + const [_, ownerName, repoName] = /([^/]+)\/([^/]+)/.exec(pathname) || []; + return {ownerName, repoName}; } export function parseIssuePageInfo(): IssuePageInfo { @@ -165,10 +166,10 @@ export function sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } -export function isImageFile({name, type}: {name: string, type?: string}): boolean { +export function isImageFile({name, type}: {name?: string, type?: string}): boolean { return /\.(avif|jpe?g|png|gif|webp|svg|heic)$/i.test(name || '') || type?.startsWith('image/'); } -export function isVideoFile({name, type}: {name: string, type?: string}): boolean { +export function isVideoFile({name, type}: {name?: string, type?: string}): boolean { return /\.(mpe?g|mp4|mkv|webm)$/i.test(name || '') || type?.startsWith('video/'); } diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index e24cb29bac..603f967b34 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -255,12 +255,12 @@ export function loadElem(el: LoadableElement, src: string) { // it can't use other transparent polyfill patches because PaleMoon also doesn't support "addEventListener(capture)" const needSubmitEventPolyfill = typeof SubmitEvent === 'undefined'; -export function submitEventSubmitter(e) { +export function submitEventSubmitter(e: any) { e = e.originalEvent ?? e; // if the event is wrapped by jQuery, use "originalEvent", otherwise, use the event itself return needSubmitEventPolyfill ? (e.target._submitter || null) : e.submitter; } -function submitEventPolyfillListener(e) { +function submitEventPolyfillListener(e: DOMEvent<Event>) { const form = e.target.closest('form'); if (!form) return; form._submitter = e.target.closest('button:not([type]), button[type="submit"], input[type="submit"]'); diff --git a/web_src/js/utils/image.test.ts b/web_src/js/utils/image.test.ts index da0605f1d0..49856c891c 100644 --- a/web_src/js/utils/image.test.ts +++ b/web_src/js/utils/image.test.ts @@ -4,7 +4,7 @@ const pngNoPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA const pngPhys = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAEElEQVQI12OQNZcAIgYIBQAL8gGxdzzM0A=='; const pngEmpty = 'data:image/png;base64,'; -async function dataUriToBlob(datauri) { +async function dataUriToBlob(datauri: string) { return await (await globalThis.fetch(datauri)).blob(); } diff --git a/web_src/js/utils/time.ts b/web_src/js/utils/time.ts index 6951ebfedb..c63498345f 100644 --- a/web_src/js/utils/time.ts +++ b/web_src/js/utils/time.ts @@ -54,7 +54,7 @@ export type DayDataObject = { } export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayDataObject): DayData[] { - const result = {}; + const result: Record<string, any> = {}; for (const startDay of startDays) { result[startDay] = data[startDay] || {'week': startDay, 'additions': 0, 'deletions': 0, 'commits': 0}; diff --git a/web_src/js/webcomponents/absolute-date.ts b/web_src/js/webcomponents/absolute-date.ts index 8eb1c3e37e..23a8606673 100644 --- a/web_src/js/webcomponents/absolute-date.ts +++ b/web_src/js/webcomponents/absolute-date.ts @@ -15,7 +15,7 @@ window.customElements.define('absolute-date', class extends HTMLElement { initialized = false; update = () => { - const opt: Intl.DateTimeFormatOptions = {}; + const opt: Record<string, string> = {}; for (const attr of ['year', 'month', 'weekday', 'day']) { if (this.getAttribute(attr)) opt[attr] = this.getAttribute(attr); }