mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-02 15:09:33 -05:00
Merge branch 'main' into feat-version-arch
This commit is contained in:
commit
39efb64c1d
@ -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)
|
||||
|
@ -790,10 +790,13 @@ LEVEL = Info
|
||||
;; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token
|
||||
;ENABLE_BASIC_AUTHENTICATION = true
|
||||
;;
|
||||
;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 login methods.
|
||||
;; Show the password sign-in form (for password-based login), otherwise, only show OAuth2 or passkey login methods if they are enabled.
|
||||
;; If you set it to false, maybe it also needs to set ENABLE_BASIC_AUTHENTICATION to false to completely disable password-based authentication.
|
||||
;ENABLE_PASSWORD_SIGNIN_FORM = true
|
||||
;;
|
||||
;; Allow users to sign-in with a passkey
|
||||
;ENABLE_PASSKEY_AUTHENTICATION = true
|
||||
;;
|
||||
;; More detail: https://github.com/gogits/gogs/issues/165
|
||||
;ENABLE_REVERSE_PROXY_AUTHENTICATION = false
|
||||
; Enable this to allow reverse proxy authentication for API requests, the reverse proxy is responsible for ensuring that no CSRF is possible.
|
||||
@ -1126,6 +1129,9 @@ LEVEL = Info
|
||||
;; In default merge messages only include approvers who are official
|
||||
;DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY = true
|
||||
;;
|
||||
;; In default squash-merge messages include the commit message of all commits comprising the pull request.
|
||||
;POPULATE_SQUASH_COMMENT_WITH_COMMIT_MESSAGES = false
|
||||
;;
|
||||
;; Add co-authored-by and co-committed-by trailers if committer does not match author
|
||||
;ADD_CO_COMMITTER_TRAILERS = true
|
||||
;;
|
||||
|
@ -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)
|
||||
|
@ -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"),
|
||||
})
|
||||
|
@ -167,186 +167,39 @@ func (w *Webhook) UpdateEvent() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// HasCreateEvent returns true if hook enabled create event.
|
||||
func (w *Webhook) HasCreateEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Create)
|
||||
func (w *Webhook) HasEvent(evt webhook_module.HookEventType) bool {
|
||||
if w.SendEverything {
|
||||
return true
|
||||
}
|
||||
|
||||
// HasDeleteEvent returns true if hook enabled delete event.
|
||||
func (w *Webhook) HasDeleteEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Delete)
|
||||
if w.PushOnly {
|
||||
return evt == webhook_module.HookEventPush
|
||||
}
|
||||
|
||||
// HasForkEvent returns true if hook enabled fork event.
|
||||
func (w *Webhook) HasForkEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Fork)
|
||||
}
|
||||
|
||||
// HasIssuesEvent returns true if hook enabled issues event.
|
||||
func (w *Webhook) HasIssuesEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Issues)
|
||||
}
|
||||
|
||||
// HasIssuesAssignEvent returns true if hook enabled issues assign event.
|
||||
func (w *Webhook) HasIssuesAssignEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.IssueAssign)
|
||||
}
|
||||
|
||||
// HasIssuesLabelEvent returns true if hook enabled issues label event.
|
||||
func (w *Webhook) HasIssuesLabelEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.IssueLabel)
|
||||
}
|
||||
|
||||
// HasIssuesMilestoneEvent returns true if hook enabled issues milestone event.
|
||||
func (w *Webhook) HasIssuesMilestoneEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.IssueMilestone)
|
||||
}
|
||||
|
||||
// HasIssueCommentEvent returns true if hook enabled issue_comment event.
|
||||
func (w *Webhook) HasIssueCommentEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.IssueComment)
|
||||
}
|
||||
|
||||
// HasPushEvent returns true if hook enabled push event.
|
||||
func (w *Webhook) HasPushEvent() bool {
|
||||
return w.PushOnly || w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Push)
|
||||
}
|
||||
|
||||
// HasPullRequestEvent returns true if hook enabled pull request event.
|
||||
func (w *Webhook) HasPullRequestEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequest)
|
||||
}
|
||||
|
||||
// HasPullRequestAssignEvent returns true if hook enabled pull request assign event.
|
||||
func (w *Webhook) HasPullRequestAssignEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestAssign)
|
||||
}
|
||||
|
||||
// HasPullRequestLabelEvent returns true if hook enabled pull request label event.
|
||||
func (w *Webhook) HasPullRequestLabelEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestLabel)
|
||||
}
|
||||
|
||||
// HasPullRequestMilestoneEvent returns true if hook enabled pull request milestone event.
|
||||
func (w *Webhook) HasPullRequestMilestoneEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestMilestone)
|
||||
}
|
||||
|
||||
// HasPullRequestCommentEvent returns true if hook enabled pull_request_comment event.
|
||||
func (w *Webhook) HasPullRequestCommentEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestComment)
|
||||
}
|
||||
|
||||
// HasPullRequestApprovedEvent returns true if hook enabled pull request review event.
|
||||
func (w *Webhook) HasPullRequestApprovedEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestReview)
|
||||
}
|
||||
|
||||
// HasPullRequestRejectedEvent returns true if hook enabled pull request review event.
|
||||
func (w *Webhook) HasPullRequestRejectedEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestReview)
|
||||
}
|
||||
|
||||
// HasPullRequestReviewCommentEvent returns true if hook enabled pull request review event.
|
||||
func (w *Webhook) HasPullRequestReviewCommentEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestReview)
|
||||
}
|
||||
|
||||
// HasPullRequestSyncEvent returns true if hook enabled pull request sync event.
|
||||
func (w *Webhook) HasPullRequestSyncEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestSync)
|
||||
}
|
||||
|
||||
// HasWikiEvent returns true if hook enabled wiki event.
|
||||
func (w *Webhook) HasWikiEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvent.Wiki)
|
||||
}
|
||||
|
||||
// HasReleaseEvent returns if hook enabled release event.
|
||||
func (w *Webhook) HasReleaseEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Release)
|
||||
}
|
||||
|
||||
// HasRepositoryEvent returns if hook enabled repository event.
|
||||
func (w *Webhook) HasRepositoryEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Repository)
|
||||
}
|
||||
|
||||
// HasPackageEvent returns if hook enabled package event.
|
||||
func (w *Webhook) HasPackageEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.Package)
|
||||
}
|
||||
|
||||
// HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event.
|
||||
func (w *Webhook) HasPullRequestReviewRequestEvent() bool {
|
||||
return w.SendEverything ||
|
||||
(w.ChooseEvents && w.HookEvents.PullRequestReviewRequest)
|
||||
}
|
||||
|
||||
// EventCheckers returns event checkers
|
||||
func (w *Webhook) EventCheckers() []struct {
|
||||
Has func() bool
|
||||
Type webhook_module.HookEventType
|
||||
} {
|
||||
return []struct {
|
||||
Has func() bool
|
||||
Type webhook_module.HookEventType
|
||||
}{
|
||||
{w.HasCreateEvent, webhook_module.HookEventCreate},
|
||||
{w.HasDeleteEvent, webhook_module.HookEventDelete},
|
||||
{w.HasForkEvent, webhook_module.HookEventFork},
|
||||
{w.HasPushEvent, webhook_module.HookEventPush},
|
||||
{w.HasIssuesEvent, webhook_module.HookEventIssues},
|
||||
{w.HasIssuesAssignEvent, webhook_module.HookEventIssueAssign},
|
||||
{w.HasIssuesLabelEvent, webhook_module.HookEventIssueLabel},
|
||||
{w.HasIssuesMilestoneEvent, webhook_module.HookEventIssueMilestone},
|
||||
{w.HasIssueCommentEvent, webhook_module.HookEventIssueComment},
|
||||
{w.HasPullRequestEvent, webhook_module.HookEventPullRequest},
|
||||
{w.HasPullRequestAssignEvent, webhook_module.HookEventPullRequestAssign},
|
||||
{w.HasPullRequestLabelEvent, webhook_module.HookEventPullRequestLabel},
|
||||
{w.HasPullRequestMilestoneEvent, webhook_module.HookEventPullRequestMilestone},
|
||||
{w.HasPullRequestCommentEvent, webhook_module.HookEventPullRequestComment},
|
||||
{w.HasPullRequestApprovedEvent, webhook_module.HookEventPullRequestReviewApproved},
|
||||
{w.HasPullRequestRejectedEvent, webhook_module.HookEventPullRequestReviewRejected},
|
||||
{w.HasPullRequestCommentEvent, webhook_module.HookEventPullRequestReviewComment},
|
||||
{w.HasPullRequestSyncEvent, webhook_module.HookEventPullRequestSync},
|
||||
{w.HasWikiEvent, webhook_module.HookEventWiki},
|
||||
{w.HasRepositoryEvent, webhook_module.HookEventRepository},
|
||||
{w.HasReleaseEvent, webhook_module.HookEventRelease},
|
||||
{w.HasPackageEvent, webhook_module.HookEventPackage},
|
||||
{w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest},
|
||||
checkEvt := evt
|
||||
switch evt {
|
||||
case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment:
|
||||
checkEvt = webhook_module.HookEventPullRequestReview
|
||||
}
|
||||
return w.HookEvents[checkEvt]
|
||||
}
|
||||
|
||||
// EventsArray returns an array of hook events
|
||||
func (w *Webhook) EventsArray() []string {
|
||||
events := make([]string, 0, 7)
|
||||
if w.SendEverything {
|
||||
events := make([]string, 0, len(webhook_module.AllEvents()))
|
||||
for _, evt := range webhook_module.AllEvents() {
|
||||
events = append(events, string(evt))
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
for _, c := range w.EventCheckers() {
|
||||
if c.Has() {
|
||||
events = append(events, string(c.Type))
|
||||
if w.PushOnly {
|
||||
return []string{string(webhook_module.HookEventPush)}
|
||||
}
|
||||
|
||||
events := make([]string, 0, len(w.HookEvents))
|
||||
for event, enabled := range w.HookEvents {
|
||||
if enabled {
|
||||
events = append(events, string(event))
|
||||
}
|
||||
}
|
||||
return events
|
||||
|
@ -54,9 +54,9 @@ func TestWebhook_UpdateEvent(t *testing.T) {
|
||||
SendEverything: false,
|
||||
ChooseEvents: false,
|
||||
HookEvents: webhook_module.HookEvents{
|
||||
Create: false,
|
||||
Push: true,
|
||||
PullRequest: false,
|
||||
webhook_module.HookEventCreate: false,
|
||||
webhook_module.HookEventPush: true,
|
||||
webhook_module.HookEventPullRequest: false,
|
||||
},
|
||||
}
|
||||
webhook.HookEvent = hookEvent
|
||||
@ -68,13 +68,13 @@ func TestWebhook_UpdateEvent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWebhook_EventsArray(t *testing.T) {
|
||||
assert.Equal(t, []string{
|
||||
assert.EqualValues(t, []string{
|
||||
"create", "delete", "fork", "push",
|
||||
"issues", "issue_assign", "issue_label", "issue_milestone", "issue_comment",
|
||||
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
|
||||
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
|
||||
"pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release",
|
||||
"package", "pull_request_review_request",
|
||||
"pull_request_review_comment", "pull_request_sync", "pull_request_review_request", "wiki", "repository", "release",
|
||||
"package", "status",
|
||||
},
|
||||
(&Webhook{
|
||||
HookEvent: &webhook_module.HookEvent{SendEverything: true},
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
|
32
modules/gtprof/event.go
Normal file
32
modules/gtprof/event.go
Normal file
@ -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
|
||||
}
|
175
modules/gtprof/trace.go
Normal file
175
modules/gtprof/trace.go
Normal file
@ -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
|
||||
}
|
96
modules/gtprof/trace_builtin.go
Normal file
96
modules/gtprof/trace_builtin.go
Normal file
@ -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))
|
||||
}
|
19
modules/gtprof/trace_const.go
Normal file
19
modules/gtprof/trace_const.go
Normal file
@ -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"
|
||||
)
|
93
modules/gtprof/trace_test.go
Normal file
93
modules/gtprof/trace_test.go
Normal file
@ -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)
|
||||
}
|
@ -46,6 +46,7 @@ var Service = struct {
|
||||
RequireSignInView bool
|
||||
EnableNotifyMail bool
|
||||
EnableBasicAuth bool
|
||||
EnablePasskeyAuth bool
|
||||
EnableReverseProxyAuth bool
|
||||
EnableReverseProxyAuthAPI bool
|
||||
EnableReverseProxyAutoRegister bool
|
||||
@ -161,6 +162,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
||||
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
|
||||
Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
|
||||
Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").MustBool(true)
|
||||
Service.EnablePasskeyAuth = sec.Key("ENABLE_PASSKEY_AUTHENTICATION").MustBool(true)
|
||||
Service.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
|
||||
Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool()
|
||||
Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool()
|
||||
|
73
modules/tailmsg/talimsg.go
Normal file
73
modules/tailmsg/talimsg.go
Normal file
@ -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),
|
||||
}
|
||||
})
|
@ -6,6 +6,9 @@ package routing
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/gtprof"
|
||||
"code.gitea.io/gitea/modules/reqctx"
|
||||
)
|
||||
|
||||
type contextKeyType struct{}
|
||||
@ -14,10 +17,12 @@ var contextKey contextKeyType
|
||||
|
||||
// RecordFuncInfo records a func info into context
|
||||
func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) {
|
||||
// TODO: reqCtx := reqctx.FromContext(ctx), add trace support
|
||||
end = func() {}
|
||||
|
||||
// save the func info into the context record
|
||||
if reqCtx := reqctx.FromContext(ctx); reqCtx != nil {
|
||||
var traceSpan *gtprof.TraceSpan
|
||||
traceSpan, end = gtprof.GetTracer().StartInContext(reqCtx, "http.func")
|
||||
traceSpan.SetAttributeString("func", funcInfo.shortName)
|
||||
}
|
||||
if record, ok := ctx.Value(contextKey).(*requestRecord); ok {
|
||||
record.lock.Lock()
|
||||
record.funcInfo = funcInfo
|
||||
|
20
modules/webhook/events.go
Normal file
20
modules/webhook/events.go
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
type HookEvents map[HookEventType]bool
|
||||
|
||||
func (he HookEvents) Get(evt HookEventType) bool {
|
||||
return he[evt]
|
||||
}
|
||||
|
||||
// HookEvent represents events that will delivery hook.
|
||||
type HookEvent struct {
|
||||
PushOnly bool `json:"push_only"`
|
||||
SendEverything bool `json:"send_everything"`
|
||||
ChooseEvents bool `json:"choose_events"`
|
||||
BranchFilter string `json:"branch_filter"`
|
||||
|
||||
HookEvents `json:"events"`
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package webhook
|
||||
|
||||
// HookEvents is a set of web hook events
|
||||
type HookEvents struct {
|
||||
Create bool `json:"create"`
|
||||
Delete bool `json:"delete"`
|
||||
Fork bool `json:"fork"`
|
||||
Issues bool `json:"issues"`
|
||||
IssueAssign bool `json:"issue_assign"`
|
||||
IssueLabel bool `json:"issue_label"`
|
||||
IssueMilestone bool `json:"issue_milestone"`
|
||||
IssueComment bool `json:"issue_comment"`
|
||||
Push bool `json:"push"`
|
||||
PullRequest bool `json:"pull_request"`
|
||||
PullRequestAssign bool `json:"pull_request_assign"`
|
||||
PullRequestLabel bool `json:"pull_request_label"`
|
||||
PullRequestMilestone bool `json:"pull_request_milestone"`
|
||||
PullRequestComment bool `json:"pull_request_comment"`
|
||||
PullRequestReview bool `json:"pull_request_review"`
|
||||
PullRequestSync bool `json:"pull_request_sync"`
|
||||
PullRequestReviewRequest bool `json:"pull_request_review_request"`
|
||||
Wiki bool `json:"wiki"`
|
||||
Repository bool `json:"repository"`
|
||||
Release bool `json:"release"`
|
||||
Package bool `json:"package"`
|
||||
}
|
||||
|
||||
// HookEvent represents events that will delivery hook.
|
||||
type HookEvent struct {
|
||||
PushOnly bool `json:"push_only"`
|
||||
SendEverything bool `json:"send_everything"`
|
||||
ChooseEvents bool `json:"choose_events"`
|
||||
BranchFilter string `json:"branch_filter"`
|
||||
|
||||
HookEvents `json:"events"`
|
||||
}
|
@ -31,21 +31,47 @@ const (
|
||||
HookEventRepository HookEventType = "repository"
|
||||
HookEventRelease HookEventType = "release"
|
||||
HookEventPackage HookEventType = "package"
|
||||
HookEventSchedule HookEventType = "schedule"
|
||||
HookEventStatus HookEventType = "status"
|
||||
// once a new event added here, please also added to AllEvents() function
|
||||
|
||||
// FIXME: This event should be a group of pull_request_review_xxx events
|
||||
HookEventPullRequestReview HookEventType = "pull_request_review"
|
||||
// Actions event only
|
||||
HookEventSchedule HookEventType = "schedule"
|
||||
)
|
||||
|
||||
func AllEvents() []HookEventType {
|
||||
return []HookEventType{
|
||||
HookEventCreate,
|
||||
HookEventDelete,
|
||||
HookEventFork,
|
||||
HookEventPush,
|
||||
HookEventIssues,
|
||||
HookEventIssueAssign,
|
||||
HookEventIssueLabel,
|
||||
HookEventIssueMilestone,
|
||||
HookEventIssueComment,
|
||||
HookEventPullRequest,
|
||||
HookEventPullRequestAssign,
|
||||
HookEventPullRequestLabel,
|
||||
HookEventPullRequestMilestone,
|
||||
HookEventPullRequestComment,
|
||||
HookEventPullRequestReviewApproved,
|
||||
HookEventPullRequestReviewRejected,
|
||||
HookEventPullRequestReviewComment,
|
||||
HookEventPullRequestSync,
|
||||
HookEventPullRequestReviewRequest,
|
||||
HookEventWiki,
|
||||
HookEventRepository,
|
||||
HookEventRelease,
|
||||
HookEventPackage,
|
||||
HookEventStatus,
|
||||
}
|
||||
}
|
||||
|
||||
// Event returns the HookEventType as an event string
|
||||
func (h HookEventType) Event() string {
|
||||
switch h {
|
||||
case HookEventCreate:
|
||||
return "create"
|
||||
case HookEventDelete:
|
||||
return "delete"
|
||||
case HookEventFork:
|
||||
return "fork"
|
||||
case HookEventPush:
|
||||
return "push"
|
||||
case HookEventIssues, HookEventIssueAssign, HookEventIssueLabel, HookEventIssueMilestone:
|
||||
return "issues"
|
||||
case HookEventPullRequest, HookEventPullRequestAssign, HookEventPullRequestLabel, HookEventPullRequestMilestone,
|
||||
@ -59,14 +85,9 @@ func (h HookEventType) Event() string {
|
||||
return "pull_request_rejected"
|
||||
case HookEventPullRequestReviewComment:
|
||||
return "pull_request_comment"
|
||||
case HookEventWiki:
|
||||
return "wiki"
|
||||
case HookEventRepository:
|
||||
return "repository"
|
||||
case HookEventRelease:
|
||||
return "release"
|
||||
default:
|
||||
return string(h)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h HookEventType) IsPullRequest() bool {
|
||||
|
@ -1115,6 +1115,7 @@ blame.ignore_revs=Ignorování revizí v <a href="%s">.git-blame-ignorerevs</a>.
|
||||
blame.ignore_revs.failed=Nepodařilo se ignorovat revize v <a href="%s">.git-blame-ignore-revs</a>.
|
||||
user_search_tooltip=Zobrazí maximálně 30 uživatelů
|
||||
|
||||
tree_path_not_found=Cesta %[1]s neexistuje v %[2]s
|
||||
|
||||
transfer.accept=Přijmout převod
|
||||
transfer.accept_desc=Převést do „%s“
|
||||
@ -1683,13 +1684,16 @@ 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>%[1]s</b> %[2]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>%[1]s</b> %[2]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>%[1]s</b> %[2]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
|
||||
@ -1947,6 +1951,8 @@ pulls.recently_pushed_new_branches=Nahráli jste větev <strong>%[1]s</strong> %
|
||||
pulls.upstream_diverging_prompt_behind_1=Tato větev je %[1]d commit pozadu za %[2]s
|
||||
pulls.upstream_diverging_prompt_behind_n=Tato větev je %[1]d commitů pozadu za %[2]s
|
||||
pulls.upstream_diverging_prompt_base_newer=Hlavní větev %s má nové změny
|
||||
pulls.upstream_diverging_merge=Synchornizovat rozštěpení
|
||||
pulls.upstream_diverging_merge_confirm=Chcete sloučit „%[1]s“ do „%[2]s“?
|
||||
|
||||
pull.deleted_branch=(odstraněno):%s
|
||||
pull.agit_documentation=Prohlédněte si dokumentaci o AGit
|
||||
@ -2152,6 +2158,7 @@ settings.advanced_settings=Pokročilá nastavení
|
||||
settings.wiki_desc=Povolit Wiki repozitáře
|
||||
settings.use_internal_wiki=Používat vestavěnou Wiki
|
||||
settings.default_wiki_branch_name=Výchozí název větve Wiki
|
||||
settings.default_permission_everyone_access=Výchozí přístupová práva pro všechny přihlášené uživatele:
|
||||
settings.failed_to_change_default_wiki_branch=Změna výchozí větve wiki se nezdařila.
|
||||
settings.use_external_wiki=Používat externí Wiki
|
||||
settings.external_wiki_url=URL externí Wiki
|
||||
@ -2706,6 +2713,8 @@ branch.create_branch_operation=Vytvořit větev
|
||||
branch.new_branch=Vytvořit novou větev
|
||||
branch.new_branch_from=Vytvořit novou větev z „%s“
|
||||
branch.renamed=Větev %s byla přejmenována na %s.
|
||||
branch.rename_default_or_protected_branch_error=Pouze administrátoři mohou přejmenovat výchozí nebo chráněné větve.
|
||||
branch.rename_protected_branch_failed=Tato větev je chráněna pravidly ochrany založenými na zástupném vzoru.
|
||||
|
||||
tag.create_tag=Vytvořit značku %s
|
||||
tag.create_tag_operation=Vytvořit značku
|
||||
@ -3358,6 +3367,8 @@ monitor.previous=Předešlý čas spuštění
|
||||
monitor.execute_times=Vykonání
|
||||
monitor.process=Spuštěné procesy
|
||||
monitor.stacktrace=Výpisy zásobníku
|
||||
monitor.trace=Trasovat
|
||||
monitor.performance_logs=Výkonnostní logy
|
||||
monitor.processes_count=%d procesů
|
||||
monitor.download_diagnosis_report=Stáhnout diagnosttickou zprávu
|
||||
monitor.desc=Popis
|
||||
@ -3366,7 +3377,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
|
||||
@ -3563,6 +3573,8 @@ 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.images=Obrázky
|
||||
container.digest=Výběr
|
||||
container.multi_arch=OS/architektura
|
||||
container.layers=Vrstvy obrazů
|
||||
container.labels=Štítky
|
||||
|
@ -3356,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
|
||||
|
@ -3368,6 +3368,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
|
||||
@ -3376,7 +3378,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
|
||||
|
@ -3367,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
|
||||
|
@ -3368,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
|
||||
|
@ -244,6 +244,7 @@ license_desc=Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">%[
|
||||
|
||||
[install]
|
||||
install=インストール
|
||||
installing_desc=インストール中です、お待ちください...
|
||||
title=初期設定
|
||||
docker_helper=GiteaをDocker内で実行する場合は、設定を変更する前に<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を読んでください。
|
||||
require_db_desc=Giteaには、MySQL、PostgreSQL、MSSQL、SQLite3、またはTiDB(MySQL プロトコル) が必要です。
|
||||
@ -1015,6 +1016,8 @@ new_repo_helper=リポジトリには、プロジェクトのすべてのファ
|
||||
owner=オーナー
|
||||
owner_helper=リポジトリ数の上限により、一部の組織はドロップダウンに表示されない場合があります。
|
||||
repo_name=リポジトリ名
|
||||
repo_name_profile_public_hint=.profile は特別なリポジトリで、これを使用して、あなたの組織の公開プロフィール(誰でも閲覧可能)に README.md を追加することができます。 利用を開始するには、必ず公開リポジトリとし、プロフィールディレクトリにREADMEを追加して初期化してください。
|
||||
repo_name_profile_private_hint=.profile-private は特別なリポジトリで、これを使用して、あなたの組織のメンバー向けプロフィール(組織メンバーのみ閲覧可能)に README.md を追加することができます。 利用を開始するには、必ずプライベートリポジトリとし、プロフィールディレクトリにREADMEを追加して初期化してください。
|
||||
repo_name_helper=リポジトリ名は、短く、覚えやすく、他と重複しないキーワードを使用しましょう。 リポジトリ名を ".profile" または ".profile-private" にして README.md を追加すると、ユーザーや組織のプロフィールとなります。
|
||||
repo_size=リポジトリサイズ
|
||||
template=テンプレート
|
||||
@ -3364,7 +3367,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=キュー
|
||||
|
@ -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
|
||||
|
@ -1952,6 +1952,7 @@ pulls.upstream_diverging_prompt_behind_1=Este ramo está %[1]d cometimento atrá
|
||||
pulls.upstream_diverging_prompt_behind_n=Este ramo está %[1]d cometimentos atrás de %[2]s
|
||||
pulls.upstream_diverging_prompt_base_newer=O ramo base %s tem novas modificações
|
||||
pulls.upstream_diverging_merge=Sincronizar derivação
|
||||
pulls.upstream_diverging_merge_confirm=Gostaria de integrar "%[1]s" em "%[2]s"?
|
||||
|
||||
pull.deleted_branch=(eliminado):%s
|
||||
pull.agit_documentation=Rever a documentação sobre o AGit
|
||||
@ -3366,6 +3367,8 @@ monitor.previous=Execução anterior
|
||||
monitor.execute_times=Execuções
|
||||
monitor.process=Processos em execução
|
||||
monitor.stacktrace=Vestígios da pilha
|
||||
monitor.trace=Rastreio
|
||||
monitor.performance_logs=Registos de desempenho
|
||||
monitor.processes_count=%d processos
|
||||
monitor.download_diagnosis_report=Descarregar relatório de diagnóstico
|
||||
monitor.desc=Descrição
|
||||
@ -3374,7 +3377,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
|
||||
|
@ -3356,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=队列
|
||||
|
@ -3347,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=佇列
|
||||
|
988
package-lock.json
generated
988
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -32,7 +32,7 @@
|
||||
"htmx.org": "2.0.4",
|
||||
"idiomorph": "0.4.0",
|
||||
"jquery": "3.7.1",
|
||||
"katex": "0.16.20",
|
||||
"katex": "0.16.21",
|
||||
"license-checker-webpack-plugin": "0.2.1",
|
||||
"mermaid": "11.4.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
@ -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.20.0",
|
||||
"@typescript-eslint/parser": "8.20.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.21.0",
|
||||
"@typescript-eslint/parser": "8.21.0",
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-import-resolver-typescript": "3.7.0",
|
||||
@ -98,7 +98,7 @@
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
"eslint-plugin-vue-scoped-css": "2.9.0",
|
||||
"eslint-plugin-wc": "2.2.0",
|
||||
"happy-dom": "16.6.0",
|
||||
"happy-dom": "16.7.2",
|
||||
"markdownlint-cli": "0.43.0",
|
||||
"nolyfill": "1.0.43",
|
||||
"postcss-html": "1.8.0",
|
||||
@ -107,10 +107,10 @@
|
||||
"stylelint-declaration-strict-value": "1.10.7",
|
||||
"stylelint-value-no-unknown-custom-properties": "6.0.1",
|
||||
"svgo": "3.3.2",
|
||||
"type-fest": "4.32.0",
|
||||
"type-fest": "4.33.0",
|
||||
"updates": "16.4.1",
|
||||
"vite-string-plugin": "1.3.4",
|
||||
"vitest": "2.1.8",
|
||||
"vite-string-plugin": "1.4.3",
|
||||
"vitest": "3.0.3",
|
||||
"vue-tsc": "2.2.0"
|
||||
},
|
||||
"browserslist": [
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -185,26 +185,27 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
|
||||
HookEvent: &webhook_module.HookEvent{
|
||||
ChooseEvents: true,
|
||||
HookEvents: webhook_module.HookEvents{
|
||||
Create: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true),
|
||||
Delete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true),
|
||||
Fork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true),
|
||||
Issues: issuesHook(form.Events, "issues_only"),
|
||||
IssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)),
|
||||
IssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)),
|
||||
IssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)),
|
||||
IssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)),
|
||||
Push: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true),
|
||||
PullRequest: pullHook(form.Events, "pull_request_only"),
|
||||
PullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)),
|
||||
PullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)),
|
||||
PullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)),
|
||||
PullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)),
|
||||
PullRequestReview: pullHook(form.Events, "pull_request_review"),
|
||||
PullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)),
|
||||
PullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)),
|
||||
Wiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true),
|
||||
Repository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true),
|
||||
Release: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true),
|
||||
webhook_module.HookEventCreate: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true),
|
||||
webhook_module.HookEventDelete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true),
|
||||
webhook_module.HookEventFork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true),
|
||||
webhook_module.HookEventIssues: issuesHook(form.Events, "issues_only"),
|
||||
webhook_module.HookEventIssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)),
|
||||
webhook_module.HookEventIssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)),
|
||||
webhook_module.HookEventIssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)),
|
||||
webhook_module.HookEventIssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)),
|
||||
webhook_module.HookEventPush: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true),
|
||||
webhook_module.HookEventPullRequest: pullHook(form.Events, "pull_request_only"),
|
||||
webhook_module.HookEventPullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)),
|
||||
webhook_module.HookEventPullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)),
|
||||
webhook_module.HookEventPullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)),
|
||||
webhook_module.HookEventPullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)),
|
||||
webhook_module.HookEventPullRequestReview: pullHook(form.Events, "pull_request_review"),
|
||||
webhook_module.HookEventPullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)),
|
||||
webhook_module.HookEventPullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)),
|
||||
webhook_module.HookEventWiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true),
|
||||
webhook_module.HookEventRepository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true),
|
||||
webhook_module.HookEventRelease: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true),
|
||||
webhook_module.HookEventStatus: util.SliceContainsString(form.Events, string(webhook_module.HookEventStatus), true),
|
||||
},
|
||||
BranchFilter: form.BranchFilter,
|
||||
},
|
||||
@ -356,14 +357,13 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
|
||||
w.PushOnly = false
|
||||
w.SendEverything = false
|
||||
w.ChooseEvents = true
|
||||
w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true)
|
||||
w.Push = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true)
|
||||
w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true)
|
||||
w.Delete = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true)
|
||||
w.Fork = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true)
|
||||
w.Repository = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true)
|
||||
w.Wiki = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true)
|
||||
w.Release = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true)
|
||||
w.HookEvents[webhook_module.HookEventCreate] = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true)
|
||||
w.HookEvents[webhook_module.HookEventPush] = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true)
|
||||
w.HookEvents[webhook_module.HookEventDelete] = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true)
|
||||
w.HookEvents[webhook_module.HookEventFork] = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true)
|
||||
w.HookEvents[webhook_module.HookEventRepository] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true)
|
||||
w.HookEvents[webhook_module.HookEventWiki] = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true)
|
||||
w.HookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true)
|
||||
w.BranchFilter = form.BranchFilter
|
||||
|
||||
err := w.SetHeaderAuthorization(form.AuthorizationHeader)
|
||||
@ -373,21 +373,20 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
|
||||
}
|
||||
|
||||
// Issues
|
||||
w.Issues = issuesHook(form.Events, "issues_only")
|
||||
w.IssueAssign = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign))
|
||||
w.IssueLabel = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel))
|
||||
w.IssueMilestone = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone))
|
||||
w.IssueComment = issuesHook(form.Events, string(webhook_module.HookEventIssueComment))
|
||||
w.HookEvents[webhook_module.HookEventIssues] = issuesHook(form.Events, "issues_only")
|
||||
w.HookEvents[webhook_module.HookEventIssueAssign] = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign))
|
||||
w.HookEvents[webhook_module.HookEventIssueLabel] = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel))
|
||||
w.HookEvents[webhook_module.HookEventIssueMilestone] = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone))
|
||||
w.HookEvents[webhook_module.HookEventIssueComment] = issuesHook(form.Events, string(webhook_module.HookEventIssueComment))
|
||||
|
||||
// Pull requests
|
||||
w.PullRequest = pullHook(form.Events, "pull_request_only")
|
||||
w.PullRequestAssign = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign))
|
||||
w.PullRequestLabel = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel))
|
||||
w.PullRequestMilestone = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone))
|
||||
w.PullRequestComment = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment))
|
||||
w.PullRequestReview = pullHook(form.Events, "pull_request_review")
|
||||
w.PullRequestReviewRequest = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest))
|
||||
w.PullRequestSync = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync))
|
||||
w.HookEvents[webhook_module.HookEventPullRequest] = pullHook(form.Events, "pull_request_only")
|
||||
w.HookEvents[webhook_module.HookEventPullRequestAssign] = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign))
|
||||
w.HookEvents[webhook_module.HookEventPullRequestLabel] = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel))
|
||||
w.HookEvents[webhook_module.HookEventPullRequestMilestone] = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone))
|
||||
w.HookEvents[webhook_module.HookEventPullRequestReview] = pullHook(form.Events, "pull_request_review")
|
||||
w.HookEvents[webhook_module.HookEventPullRequestReviewRequest] = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest))
|
||||
w.HookEvents[webhook_module.HookEventPullRequestSync] = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync))
|
||||
|
||||
if err := w.UpdateEvent(); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateEvent", err)
|
||||
|
@ -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"
|
||||
@ -52,6 +53,14 @@ func RequestContextHandler() func(h http.Handler) http.Handler {
|
||||
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(respWriter, req, err) // it should never panic
|
||||
@ -75,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)
|
||||
})
|
||||
|
@ -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"
|
||||
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
18
routers/web/admin/perftrace.go
Normal file
18
routers/web/admin/perftrace.go
Normal file
@ -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)
|
||||
}
|
@ -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()
|
||||
|
||||
|
@ -169,6 +169,7 @@ func prepareSignInPageData(ctx *context.Context) {
|
||||
ctx.Data["PageIsLogin"] = true
|
||||
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
|
||||
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
|
||||
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
|
||||
|
||||
if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
|
||||
context.SetCaptchaData(ctx)
|
||||
|
@ -46,6 +46,7 @@ func LinkAccount(ctx *context.Context) {
|
||||
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
|
||||
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
|
||||
ctx.Data["ShowRegistrationButton"] = false
|
||||
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
|
||||
|
||||
// use this to set the right link into the signIn and signUp templates in the link_account template
|
||||
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
|
||||
@ -145,6 +146,7 @@ func LinkAccountPostSignIn(ctx *context.Context) {
|
||||
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
|
||||
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
|
||||
ctx.Data["ShowRegistrationButton"] = false
|
||||
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
|
||||
|
||||
// use this to set the right link into the signIn and signUp templates in the link_account template
|
||||
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
|
||||
@ -235,6 +237,7 @@ func LinkAccountPostRegister(ctx *context.Context) {
|
||||
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
|
||||
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
|
||||
ctx.Data["ShowRegistrationButton"] = false
|
||||
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
|
||||
|
||||
// use this to set the right link into the signIn and signUp templates in the link_account template
|
||||
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/link_account_signin"
|
||||
|
@ -50,6 +50,11 @@ func WebAuthn(ctx *context.Context) {
|
||||
|
||||
// WebAuthnPasskeyAssertion submits a WebAuthn challenge for the passkey login to the browser
|
||||
func WebAuthnPasskeyAssertion(ctx *context.Context) {
|
||||
if !setting.Service.EnablePasskeyAuth {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin()
|
||||
if err != nil {
|
||||
ctx.ServerError("webauthn.BeginDiscoverableLogin", err)
|
||||
@ -66,6 +71,11 @@ func WebAuthnPasskeyAssertion(ctx *context.Context) {
|
||||
|
||||
// WebAuthnPasskeyLogin handles the WebAuthn login process using a Passkey
|
||||
func WebAuthnPasskeyLogin(ctx *context.Context) {
|
||||
if !setting.Service.EnablePasskeyAuth {
|
||||
ctx.Error(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
sessionData, okData := ctx.Session.Get("webauthnPasskeyAssertion").(*webauthn.SessionData)
|
||||
if !okData || sessionData == nil {
|
||||
ctx.ServerError("ctx.Session.Get", errors.New("not in WebAuthn session"))
|
||||
|
@ -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})
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -163,27 +163,27 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
|
||||
SendEverything: form.SendEverything(),
|
||||
ChooseEvents: form.ChooseEvents(),
|
||||
HookEvents: webhook_module.HookEvents{
|
||||
Create: form.Create,
|
||||
Delete: form.Delete,
|
||||
Fork: form.Fork,
|
||||
Issues: form.Issues,
|
||||
IssueAssign: form.IssueAssign,
|
||||
IssueLabel: form.IssueLabel,
|
||||
IssueMilestone: form.IssueMilestone,
|
||||
IssueComment: form.IssueComment,
|
||||
Release: form.Release,
|
||||
Push: form.Push,
|
||||
PullRequest: form.PullRequest,
|
||||
PullRequestAssign: form.PullRequestAssign,
|
||||
PullRequestLabel: form.PullRequestLabel,
|
||||
PullRequestMilestone: form.PullRequestMilestone,
|
||||
PullRequestComment: form.PullRequestComment,
|
||||
PullRequestReview: form.PullRequestReview,
|
||||
PullRequestSync: form.PullRequestSync,
|
||||
PullRequestReviewRequest: form.PullRequestReviewRequest,
|
||||
Wiki: form.Wiki,
|
||||
Repository: form.Repository,
|
||||
Package: form.Package,
|
||||
webhook_module.HookEventCreate: form.Create,
|
||||
webhook_module.HookEventDelete: form.Delete,
|
||||
webhook_module.HookEventFork: form.Fork,
|
||||
webhook_module.HookEventIssues: form.Issues,
|
||||
webhook_module.HookEventIssueAssign: form.IssueAssign,
|
||||
webhook_module.HookEventIssueLabel: form.IssueLabel,
|
||||
webhook_module.HookEventIssueMilestone: form.IssueMilestone,
|
||||
webhook_module.HookEventIssueComment: form.IssueComment,
|
||||
webhook_module.HookEventRelease: form.Release,
|
||||
webhook_module.HookEventPush: form.Push,
|
||||
webhook_module.HookEventPullRequest: form.PullRequest,
|
||||
webhook_module.HookEventPullRequestAssign: form.PullRequestAssign,
|
||||
webhook_module.HookEventPullRequestLabel: form.PullRequestLabel,
|
||||
webhook_module.HookEventPullRequestMilestone: form.PullRequestMilestone,
|
||||
webhook_module.HookEventPullRequestComment: form.PullRequestComment,
|
||||
webhook_module.HookEventPullRequestReview: form.PullRequestReview,
|
||||
webhook_module.HookEventPullRequestSync: form.PullRequestSync,
|
||||
webhook_module.HookEventPullRequestReviewRequest: form.PullRequestReviewRequest,
|
||||
webhook_module.HookEventWiki: form.Wiki,
|
||||
webhook_module.HookEventRepository: form.Repository,
|
||||
webhook_module.HookEventPackage: form.Package,
|
||||
},
|
||||
BranchFilter: form.BranchFilter,
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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"])
|
||||
|
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -85,7 +85,7 @@ func (lr *accessLogRecorder) record(start time.Time, respWriter ResponseWriter,
|
||||
},
|
||||
RequestID: &requestID,
|
||||
}
|
||||
tmplData.ResponseWriter.Status = respWriter.Status()
|
||||
tmplData.ResponseWriter.Status = respWriter.WrittenStatus()
|
||||
tmplData.ResponseWriter.Size = respWriter.WrittenSize()
|
||||
err = lr.logTemplate.Execute(buf, tmplData)
|
||||
if err != nil {
|
||||
|
@ -48,10 +48,6 @@ func (t testAccessLoggerResponseWriterMock) WrittenStatus() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (t testAccessLoggerResponseWriterMock) Status() int {
|
||||
return t.WrittenStatus()
|
||||
}
|
||||
|
||||
func (t testAccessLoggerResponseWriterMock) WrittenSize() int {
|
||||
return 123123
|
||||
}
|
||||
|
@ -11,12 +11,11 @@ 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(fn func(ResponseWriter))
|
||||
Status() int
|
||||
WrittenSize() int
|
||||
}
|
||||
|
||||
@ -75,12 +74,6 @@ func (r *Response) Flush() {
|
||||
}
|
||||
}
|
||||
|
||||
// Status returns status code written
|
||||
// TODO: use WrittenStatus instead
|
||||
func (r *Response) Status() int {
|
||||
return r.status
|
||||
}
|
||||
|
||||
// WrittenStatus returned status code written
|
||||
func (r *Response) WrittenStatus() int {
|
||||
return r.status
|
||||
|
@ -219,26 +219,18 @@ type ProtectBranchPriorityForm struct {
|
||||
IDs []int64
|
||||
}
|
||||
|
||||
// __ __ ___. .__ __
|
||||
// / \ / \ ____\_ |__ | |__ ____ ____ | | __
|
||||
// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
|
||||
// \ /\ ___/| \_\ \ Y ( <_> | <_> ) <
|
||||
// \__/\ / \___ >___ /___| /\____/ \____/|__|_ \
|
||||
// \/ \/ \/ \/ \/
|
||||
|
||||
// WebhookForm form for changing web hook
|
||||
type WebhookForm struct {
|
||||
Events string
|
||||
Create bool
|
||||
Delete bool
|
||||
Fork bool
|
||||
Push bool
|
||||
Issues bool
|
||||
IssueAssign bool
|
||||
IssueLabel bool
|
||||
IssueMilestone bool
|
||||
IssueComment bool
|
||||
Release bool
|
||||
Push bool
|
||||
PullRequest bool
|
||||
PullRequestAssign bool
|
||||
PullRequestLabel bool
|
||||
@ -249,6 +241,7 @@ type WebhookForm struct {
|
||||
PullRequestReviewRequest bool
|
||||
Wiki bool
|
||||
Repository bool
|
||||
Release bool
|
||||
Package bool
|
||||
Active bool
|
||||
BranchFilter string `binding:"GlobPattern"`
|
||||
|
@ -137,16 +137,10 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, e := range w.EventCheckers() {
|
||||
if event == e.Type {
|
||||
if !e.Has() {
|
||||
if !w.HasEvent(event) {
|
||||
return nil
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.).
|
||||
// Integration webhooks (e.g. drone) still receive the required data.
|
||||
if pushEvent, ok := p.(*api.PushPayload); ok &&
|
||||
|
@ -95,7 +95,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">
|
||||
@ -107,8 +107,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>
|
||||
|
13
templates/admin/perftrace.tmpl
Normal file
13
templates/admin/perftrace.tmpl
Normal file
@ -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" .}}
|
@ -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>
|
||||
|
@ -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" .}}
|
||||
|
19
templates/admin/trace_tabs.tmpl
Normal file
19
templates/admin/trace_tabs.tmpl
Normal file
@ -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>
|
@ -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">
|
||||
|
@ -31,7 +31,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="create" type="checkbox" {{if .Webhook.Create}}checked{{end}}>
|
||||
<input name="create" type="checkbox" {{if .Webhook.HookEvents.Get "create"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_create"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_create_desc"}}</span>
|
||||
</div>
|
||||
@ -41,7 +41,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="delete" type="checkbox" {{if .Webhook.Delete}}checked{{end}}>
|
||||
<input name="delete" type="checkbox" {{if .Webhook.HookEvents.Get "delete"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_delete"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_delete_desc"}}</span>
|
||||
</div>
|
||||
@ -51,7 +51,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="fork" type="checkbox" {{if .Webhook.Fork}}checked{{end}}>
|
||||
<input name="fork" type="checkbox" {{if .Webhook.HookEvents.Get "fork"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_fork"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_fork_desc"}}</span>
|
||||
</div>
|
||||
@ -61,7 +61,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="push" type="checkbox" {{if .Webhook.Push}}checked{{end}}>
|
||||
<input name="push" type="checkbox" {{if .Webhook.HookEvents.Get "push"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_push"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_push_desc"}}</span>
|
||||
</div>
|
||||
@ -71,7 +71,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="repository" type="checkbox" {{if .Webhook.Repository}}checked{{end}}>
|
||||
<input name="repository" type="checkbox" {{if .Webhook.HookEvents.Get "repository"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_repository"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_repository_desc"}}</span>
|
||||
</div>
|
||||
@ -81,7 +81,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="release" type="checkbox" {{if .Webhook.Release}}checked{{end}}>
|
||||
<input name="release" type="checkbox" {{if .Webhook.HookEvents.Get "release"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_release"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_release_desc"}}</span>
|
||||
</div>
|
||||
@ -91,7 +91,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="package" type="checkbox" {{if .Webhook.Package}}checked{{end}}>
|
||||
<input name="package" type="checkbox" {{if .Webhook.HookEvents.Get "package"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_package"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_package_desc"}}</span>
|
||||
</div>
|
||||
@ -102,7 +102,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="wiki" type="checkbox" {{if .Webhook.Wiki}}checked{{end}}>
|
||||
<input name="wiki" type="checkbox" {{if .Webhook.HookEvents.Get "wiki"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_wiki"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_wiki_desc"}}</span>
|
||||
</div>
|
||||
@ -117,7 +117,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="issues" type="checkbox" {{if .Webhook.Issues}}checked{{end}}>
|
||||
<input name="issues" type="checkbox" {{if .Webhook.HookEvents.Get "issues"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_issues"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_issues_desc"}}</span>
|
||||
</div>
|
||||
@ -127,7 +127,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="issue_assign" type="checkbox" {{if .Webhook.IssueAssign}}checked{{end}}>
|
||||
<input name="issue_assign" type="checkbox" {{if .Webhook.HookEvents.Get "issue_assign"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_issue_assign"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_issue_assign_desc"}}</span>
|
||||
</div>
|
||||
@ -137,7 +137,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="issue_label" type="checkbox" {{if .Webhook.IssueLabel}}checked{{end}}>
|
||||
<input name="issue_label" type="checkbox" {{if .Webhook.HookEvents.Get "issue_label"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_issue_label"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_issue_label_desc"}}</span>
|
||||
</div>
|
||||
@ -147,7 +147,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="issue_milestone" type="checkbox" {{if .Webhook.IssueMilestone}}checked{{end}}>
|
||||
<input name="issue_milestone" type="checkbox" {{if .Webhook.HookEvents.Get "issue_milestone"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_issue_milestone"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_issue_milestone_desc"}}</span>
|
||||
</div>
|
||||
@ -157,7 +157,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="issue_comment" type="checkbox" {{if .Webhook.IssueComment}}checked{{end}}>
|
||||
<input name="issue_comment" type="checkbox" {{if .Webhook.HookEvents.Get "issue_comment"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_issue_comment"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_issue_comment_desc"}}</span>
|
||||
</div>
|
||||
@ -172,7 +172,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pull_request" type="checkbox" {{if .Webhook.PullRequest}}checked{{end}}>
|
||||
<input name="pull_request" type="checkbox" {{if .Webhook.HookEvents.Get "pull_request"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_pull_request"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_desc"}}</span>
|
||||
</div>
|
||||
@ -182,7 +182,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pull_request_assign" type="checkbox" {{if .Webhook.PullRequestAssign}}checked{{end}}>
|
||||
<input name="pull_request_assign" type="checkbox" {{if .Webhook.HookEvents.Get "pull_request_assign"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_pull_request_assign"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_assign_desc"}}</span>
|
||||
</div>
|
||||
@ -192,7 +192,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pull_request_label" type="checkbox" {{if .Webhook.PullRequestLabel}}checked{{end}}>
|
||||
<input name="pull_request_label" type="checkbox" {{if .Webhook.HookEvents.Get "pull_request_label"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_pull_request_label"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_label_desc"}}</span>
|
||||
</div>
|
||||
@ -202,7 +202,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pull_request_milestone" type="checkbox" {{if .Webhook.PullRequestMilestone}}checked{{end}}>
|
||||
<input name="pull_request_milestone" type="checkbox" {{if .Webhook.HookEvents.Get "pull_request_milestone"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_pull_request_milestone"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_milestone_desc"}}</span>
|
||||
</div>
|
||||
@ -212,7 +212,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pull_request_comment" type="checkbox" {{if .Webhook.PullRequestComment}}checked{{end}}>
|
||||
<input name="pull_request_comment" type="checkbox" {{if .Webhook.HookEvents.Get "pull_request_comment"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_pull_request_comment"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_comment_desc"}}</span>
|
||||
</div>
|
||||
@ -222,7 +222,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pull_request_review" type="checkbox" {{if .Webhook.PullRequestReview}}checked{{end}}>
|
||||
<input name="pull_request_review" type="checkbox" {{if .Webhook.HookEvents.Get "pull_request_review"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_pull_request_review"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_review_desc"}}</span>
|
||||
</div>
|
||||
@ -232,7 +232,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pull_request_sync" type="checkbox" {{if .Webhook.PullRequestSync}}checked{{end}}>
|
||||
<input name="pull_request_sync" type="checkbox" {{if .Webhook.HookEvents.Get "pull_request_sync"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_pull_request_sync"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_sync_desc"}}</span>
|
||||
</div>
|
||||
@ -242,7 +242,7 @@
|
||||
<div class="seven wide column">
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input name="pull_request_review_request" type="checkbox" {{if .Webhook.PullRequestReviewRequest}}checked{{end}}>
|
||||
<input name="pull_request_review_request" type="checkbox" {{if .Webhook.HookEvents.Get "pull_request_review_request"}}checked{{end}}>
|
||||
<label>{{ctx.Locale.Tr "repo.settings.event_pull_request_review_request"}}</label>
|
||||
<span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_review_request_desc"}}</span>
|
||||
</div>
|
||||
|
@ -60,10 +60,11 @@
|
||||
</div>
|
||||
|
||||
<div class="ui container fluid">
|
||||
{{template "user/auth/webauthn_error" .}}
|
||||
|
||||
<div class="ui attached segment header top tw-max-w-2xl tw-m-auto tw-flex tw-flex-col tw-items-center">
|
||||
{{if .EnablePasskeyAuth}}
|
||||
{{template "user/auth/webauthn_error" .}}
|
||||
<a class="signin-passkey">{{ctx.Locale.Tr "auth.signin_passkey"}}</a>
|
||||
{{end}}
|
||||
|
||||
{{if .ShowRegistrationButton}}
|
||||
<div class="field">
|
||||
|
@ -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
|
||||
|
@ -98,7 +98,7 @@ func TestSigninWithRememberMe(t *testing.T) {
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestEnablePasswordSignInForm(t *testing.T) {
|
||||
func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
mockLinkAccount := func(ctx *context.Context) {
|
||||
@ -141,4 +141,22 @@ func TestEnablePasswordSignInForm(t *testing.T) {
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", true)
|
||||
})
|
||||
|
||||
t.Run("EnablePasskeyAuth=false", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
defer test.MockVariableValue(&setting.Service.EnablePasskeyAuth, false)()
|
||||
|
||||
req := NewRequest(t, "GET", "/user/login")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
NewHTMLParser(t, resp.Body).AssertElement(t, ".signin-passkey", false)
|
||||
})
|
||||
|
||||
t.Run("EnablePasskeyAuth=true", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
defer test.MockVariableValue(&setting.Service.EnablePasskeyAuth, true)()
|
||||
|
||||
req := NewRequest(t, "GET", "/user/login")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
NewHTMLParser(t, resp.Body).AssertElement(t, ".signin-passkey", true)
|
||||
})
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
"stripInternal": true,
|
||||
"strict": false,
|
||||
"strictFunctionTypes": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
|
@ -130,12 +130,12 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
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 @@ export default defineComponent({
|
||||
this.searchRepos();
|
||||
},
|
||||
|
||||
changePage(page) {
|
||||
changePage(page: number) {
|
||||
this.page = page;
|
||||
if (this.page > this.finalPage) {
|
||||
this.page = this.finalPage;
|
||||
@ -256,7 +256,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
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 @@ export default defineComponent({
|
||||
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 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
|
||||
repoIcon(repo) {
|
||||
repoIcon(repo: any) {
|
||||
if (repo.fork) {
|
||||
return 'octicon-repo-forked';
|
||||
} else if (repo.mirror) {
|
||||
@ -298,7 +298,7 @@ export default defineComponent({
|
||||
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();
|
||||
|
@ -4,6 +4,22 @@ import {SvgIcon} from '../svg.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import {generateAriaId} from '../modules/fomantic/base.ts';
|
||||
|
||||
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: () => {
|
||||
@ -16,9 +32,9 @@ export default defineComponent({
|
||||
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(),
|
||||
};
|
||||
@ -71,7 +87,7 @@ export default defineComponent({
|
||||
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) {
|
||||
@ -87,7 +103,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
},
|
||||
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);
|
||||
@ -125,10 +141,11 @@ export default defineComponent({
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/** 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;
|
||||
@ -166,7 +183,7 @@ export default defineComponent({
|
||||
* 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
|
||||
|
@ -18,14 +18,14 @@ function toggleFileList() {
|
||||
}
|
||||
|
||||
function diffTypeToString(pType: number) {
|
||||
const diffTypes = {
|
||||
1: 'add',
|
||||
2: 'modify',
|
||||
3: 'del',
|
||||
4: 'rename',
|
||||
5: 'copy',
|
||||
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: number, dels: number) {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
@ -26,14 +26,14 @@ const store = diffTreeStore();
|
||||
const collapsed = ref(false);
|
||||
|
||||
function getIconForDiffType(pType: number) {
|
||||
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
|
||||
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: File) {
|
||||
|
@ -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);
|
||||
|
@ -6,6 +6,7 @@ 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)) {
|
||||
@ -77,7 +92,7 @@ export default defineComponent({
|
||||
default: '',
|
||||
},
|
||||
locale: {
|
||||
type: Object as PropType<Record<string, string>>,
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
@ -86,10 +101,10 @@ export default defineComponent({
|
||||
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,
|
||||
@ -122,7 +137,7 @@ export default defineComponent({
|
||||
// canRerun: false,
|
||||
// duration: '',
|
||||
// },
|
||||
],
|
||||
] as Array<Job>,
|
||||
commit: {
|
||||
localeCommit: '',
|
||||
localePushedBy: '',
|
||||
@ -148,7 +163,7 @@ export default defineComponent({
|
||||
// duration: '',
|
||||
// status: '',
|
||||
// }
|
||||
],
|
||||
] as Array<Step>,
|
||||
},
|
||||
};
|
||||
},
|
||||
@ -194,7 +209,7 @@ export default defineComponent({
|
||||
|
||||
// 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`
|
||||
@ -205,7 +220,7 @@ export default defineComponent({
|
||||
},
|
||||
// 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,
|
||||
@ -223,7 +238,7 @@ export default defineComponent({
|
||||
},
|
||||
// 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,
|
||||
@ -393,7 +408,7 @@ export default defineComponent({
|
||||
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 as HTMLElement).querySelectorAll(`.log-time-${type}`)) {
|
||||
toggleElem(el, this.timeVisible[`log-time-${type}`]);
|
||||
@ -422,9 +437,10 @@ export default defineComponent({
|
||||
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();
|
||||
|
@ -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';
|
||||
|
||||
|
@ -157,7 +157,7 @@ export default defineComponent({
|
||||
// @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();
|
||||
|
||||
@ -181,7 +181,7 @@ export default defineComponent({
|
||||
this.menuVisible = false;
|
||||
}
|
||||
},
|
||||
handleTabSwitch(selectedTab) {
|
||||
handleTabSwitch(selectedTab: SelectedTab) {
|
||||
this.selectedTab = selectedTab;
|
||||
this.focusSearchField();
|
||||
this.loadTabItems();
|
||||
|
@ -80,10 +80,10 @@ export default defineComponent({
|
||||
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();
|
||||
@ -99,7 +99,7 @@ export default defineComponent({
|
||||
},
|
||||
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)
|
||||
@ -158,7 +158,7 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
filterContributorWeeksByDateRange() {
|
||||
const filteredData = {};
|
||||
const filteredData: Record<string, any> = {};
|
||||
const data = this.contributorsStats;
|
||||
for (const key of Object.keys(data)) {
|
||||
const user = data[key];
|
||||
@ -196,7 +196,7 @@ export default defineComponent({
|
||||
// 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;
|
||||
@ -208,7 +208,7 @@ export default defineComponent({
|
||||
// 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;
|
||||
@ -232,8 +232,8 @@ export default defineComponent({
|
||||
},
|
||||
|
||||
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;
|
||||
|
@ -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');
|
||||
|
@ -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]')) {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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,11 +49,20 @@ 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;
|
||||
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
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;
|
||||
});
|
||||
|
@ -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()) {
|
||||
|
@ -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();
|
||||
|
@ -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) ? ' ' : '';
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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});
|
||||
|
@ -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}:`;
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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',
|
||||
|
@ -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({
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user