Merge branch 'main' into feat-version-arch

pull/33262/head
Exploding Dragon 1 week ago committed by GitHub
commit 39efb64c1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      cmd/web.go
  2. 8
      custom/conf/app.example.ini
  3. 23
      models/db/engine_hook.go
  4. 2
      models/db/engine_init.go
  5. 201
      models/webhook/webhook.go
  6. 12
      models/webhook/webhook_test.go
  7. 12
      modules/git/command.go
  8. 2
      modules/git/command_test.go
  9. 32
      modules/gtprof/event.go
  10. 175
      modules/gtprof/trace.go
  11. 96
      modules/gtprof/trace_builtin.go
  12. 19
      modules/gtprof/trace_const.go
  13. 93
      modules/gtprof/trace_test.go
  14. 2
      modules/setting/service.go
  15. 73
      modules/tailmsg/talimsg.go
  16. 11
      modules/web/routing/context.go
  17. 20
      modules/webhook/events.go
  18. 39
      modules/webhook/structs.go
  19. 53
      modules/webhook/type.go
  20. 14
      options/locale/locale_cs-CZ.ini
  21. 1
      options/locale/locale_de-DE.ini
  22. 3
      options/locale/locale_en-US.ini
  23. 1
      options/locale/locale_fr-FR.ini
  24. 1
      options/locale/locale_ga-IE.ini
  25. 4
      options/locale/locale_ja-JP.ini
  26. 1
      options/locale/locale_pl-PL.ini
  27. 4
      options/locale/locale_pt-PT.ini
  28. 1
      options/locale/locale_zh-CN.ini
  29. 1
      options/locale/locale_zh-TW.ini
  30. 988
      package-lock.json
  31. 14
      package.json
  32. 2
      routers/api/v1/repo/hook_test.go
  33. 4
      routers/api/v1/repo/repo_test.go
  34. 81
      routers/api/v1/utils/hook.go
  35. 15
      routers/common/middleware.go
  36. 1
      routers/web/admin/admin.go
  37. 18
      routers/web/admin/diagnosis.go
  38. 18
      routers/web/admin/perftrace.go
  39. 11
      routers/web/admin/stacktrace.go
  40. 1
      routers/web/auth/auth.go
  41. 3
      routers/web/auth/linkaccount.go
  42. 10
      routers/web/auth/webauthn.go
  43. 12
      routers/web/repo/issue_label_test.go
  44. 22
      routers/web/repo/setting/settings_test.go
  45. 42
      routers/web/repo/setting/webhook.go
  46. 24
      routers/web/repo/wiki_test.go
  47. 30
      routers/web/user/home.go
  48. 10
      routers/web/user/home_test.go
  49. 2
      routers/web/user/setting/account_test.go
  50. 1
      routers/web/web.go
  51. 2
      services/context/access_log.go
  52. 4
      services/context/access_log_test.go
  53. 13
      services/context/response.go
  54. 11
      services/forms/repo_form.go
  55. 10
      services/webhook/webhook.go
  56. 6
      templates/admin/navbar.tmpl
  57. 13
      templates/admin/perftrace.tmpl
  58. 5
      templates/admin/stacktrace-row.tmpl
  59. 28
      templates/admin/stacktrace.tmpl
  60. 19
      templates/admin/trace_tabs.tmpl
  61. 3
      templates/repo/clone_panel.tmpl
  62. 42
      templates/repo/settings/webhook/settings.tmpl
  63. 7
      templates/user/auth/signin_inner.tmpl
  64. 11
      tests/e2e/utils_e2e.ts
  65. 20
      tests/integration/signin_test.go
  66. 1
      tsconfig.json
  67. 16
      web_src/js/components/DashboardRepoList.vue
  68. 29
      web_src/js/components/DiffCommitSelector.vue
  69. 14
      web_src/js/components/DiffFileList.vue
  70. 11
      web_src/js/components/DiffFileTree.vue
  71. 18
      web_src/js/components/DiffFileTreeItem.vue
  72. 8
      web_src/js/components/PullRequestMergeForm.vue
  73. 44
      web_src/js/components/RepoActionView.vue
  74. 1
      web_src/js/components/RepoActivityTopAuthors.vue
  75. 4
      web_src/js/components/RepoBranchTagSelector.vue
  76. 20
      web_src/js/components/RepoContributors.vue
  77. 2
      web_src/js/components/ScopedAccessTokenSelector.vue
  78. 2
      web_src/js/features/admin/common.ts
  79. 4
      web_src/js/features/citation.ts
  80. 18
      web_src/js/features/common-button.ts
  81. 5
      web_src/js/features/common-fetch-action.ts
  82. 4
      web_src/js/features/common-form.ts
  83. 41
      web_src/js/features/comp/ComboMarkdownEditor.ts
  84. 13
      web_src/js/features/comp/EditorMarkdown.ts
  85. 53
      web_src/js/features/comp/EditorUpload.ts
  86. 2
      web_src/js/features/comp/QuickSubmit.ts
  87. 2
      web_src/js/features/comp/SearchUserBox.ts
  88. 18
      web_src/js/features/comp/TextExpander.ts
  89. 4
      web_src/js/features/contextpopup.ts
  90. 2
      web_src/js/features/copycontent.ts
  91. 18
      web_src/js/features/dropzone.ts
  92. 6
      web_src/js/features/emoji.ts
  93. 6
      web_src/js/features/file-fold.ts
  94. 2
      web_src/js/features/heatmap.ts
  95. 18
      web_src/js/features/imagediff.ts
  96. 3
      web_src/js/features/install.ts
  97. 2
      web_src/js/features/org-team.ts
  98. 8
      web_src/js/features/pull-view-file.ts
  99. 8
      web_src/js/features/repo-common.ts
  100. 2
      web_src/js/features/repo-diff.ts
  101. Some files were not shown because too many files have changed in this diff Show More

@ -18,10 +18,12 @@ import (
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/graceful" "code.gitea.io/gitea/modules/graceful"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/public" "code.gitea.io/gitea/modules/public"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers" "code.gitea.io/gitea/routers"
"code.gitea.io/gitea/routers/install" "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 // Set up Chi routes
webRoutes := routers.NormalRoutes() webRoutes := routers.NormalRoutes()
err := listen(webRoutes, true) 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 ;; Please note that setting this to false will not disable OAuth Basic or Basic authentication using a token
;ENABLE_BASIC_AUTHENTICATION = true ;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. ;; 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 ;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 ;; More detail: https://github.com/gogits/gogs/issues/165
;ENABLE_REVERSE_PROXY_AUTHENTICATION = false ;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. ; 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 ;; In default merge messages only include approvers who are official
;DEFAULT_MERGE_MESSAGE_OFFICIAL_APPROVERS_ONLY = true ;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-authored-by and co-committed-by trailers if committer does not match author
;ADD_CO_COMMITTER_TRAILERS = true ;ADD_CO_COMMITTER_TRAILERS = true
;; ;;

@ -7,23 +7,36 @@ import (
"context" "context"
"time" "time"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"xorm.io/xorm/contexts" "xorm.io/xorm/contexts"
) )
type SlowQueryHook struct { type EngineHook struct {
Threshold time.Duration Threshold time.Duration
Logger log.Logger Logger log.Logger
} }
var _ contexts.Hook = (*SlowQueryHook)(nil) var _ contexts.Hook = (*EngineHook)(nil)
func (*SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) { func (*EngineHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
return c.Ctx, nil 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 { if c.ExecuteTime >= h.Threshold {
// 8 is the amount of skips passed to runtime.Caller, so that in the log the correct function // 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) // 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) xe.SetDefaultContext(ctx)
if setting.Database.SlowQueryThreshold > 0 { if setting.Database.SlowQueryThreshold > 0 {
xe.AddHook(&SlowQueryHook{ xe.AddHook(&EngineHook{
Threshold: setting.Database.SlowQueryThreshold, Threshold: setting.Database.SlowQueryThreshold,
Logger: log.GetLogger("xorm"), Logger: log.GetLogger("xorm"),
}) })

@ -167,186 +167,39 @@ func (w *Webhook) UpdateEvent() error {
return err return err
} }
// HasCreateEvent returns true if hook enabled create event. func (w *Webhook) HasEvent(evt webhook_module.HookEventType) bool {
func (w *Webhook) HasCreateEvent() bool { if w.SendEverything {
return w.SendEverything || return true
(w.ChooseEvents && w.HookEvents.Create)
}
// HasDeleteEvent returns true if hook enabled delete event.
func (w *Webhook) HasDeleteEvent() bool {
return w.SendEverything ||
(w.ChooseEvents && w.HookEvents.Delete)
}
// 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},
} }
if w.PushOnly {
return evt == webhook_module.HookEventPush
}
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 // EventsArray returns an array of hook events
func (w *Webhook) EventsArray() []string { 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
}
if w.PushOnly {
return []string{string(webhook_module.HookEventPush)}
}
for _, c := range w.EventCheckers() { events := make([]string, 0, len(w.HookEvents))
if c.Has() { for event, enabled := range w.HookEvents {
events = append(events, string(c.Type)) if enabled {
events = append(events, string(event))
} }
} }
return events return events

@ -54,9 +54,9 @@ func TestWebhook_UpdateEvent(t *testing.T) {
SendEverything: false, SendEverything: false,
ChooseEvents: false, ChooseEvents: false,
HookEvents: webhook_module.HookEvents{ HookEvents: webhook_module.HookEvents{
Create: false, webhook_module.HookEventCreate: false,
Push: true, webhook_module.HookEventPush: true,
PullRequest: false, webhook_module.HookEventPullRequest: false,
}, },
} }
webhook.HookEvent = hookEvent webhook.HookEvent = hookEvent
@ -68,13 +68,13 @@ func TestWebhook_UpdateEvent(t *testing.T) {
} }
func TestWebhook_EventsArray(t *testing.T) { func TestWebhook_EventsArray(t *testing.T) {
assert.Equal(t, []string{ assert.EqualValues(t, []string{
"create", "delete", "fork", "push", "create", "delete", "fork", "push",
"issues", "issue_assign", "issue_label", "issue_milestone", "issue_comment", "issues", "issue_assign", "issue_label", "issue_milestone", "issue_comment",
"pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone", "pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
"pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected", "pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
"pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release", "pull_request_review_comment", "pull_request_sync", "pull_request_review_request", "wiki", "repository", "release",
"package", "pull_request_review_request", "package", "status",
}, },
(&Webhook{ (&Webhook{
HookEvent: &webhook_module.HookEvent{SendEverything: true}, HookEvent: &webhook_module.HookEvent{SendEverything: true},

@ -18,6 +18,7 @@ import (
"time" "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/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/log"
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
@ -54,7 +55,7 @@ func logArgSanitize(arg string) string {
} else if filepath.IsAbs(arg) { } else if filepath.IsAbs(arg) {
base := filepath.Base(arg) base := filepath.Base(arg)
dir := filepath.Dir(arg) dir := filepath.Dir(arg)
return filepath.Join(filepath.Base(dir), base) return ".../" + filepath.Join(filepath.Base(dir), base)
} }
return arg return arg
} }
@ -295,15 +296,20 @@ func (c *Command) run(skip int, opts *RunOpts) error {
timeout = defaultCommandExecutionTimeout timeout = defaultCommandExecutionTimeout
} }
var desc string cmdLogString := c.LogString()
callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */) callerInfo := util.CallerFuncName(1 /* util */ + 1 /* this */ + skip /* parent */)
if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 { if pos := strings.LastIndex(callerInfo, "/"); pos >= 0 {
callerInfo = callerInfo[pos+1:] callerInfo = callerInfo[pos+1:]
} }
// these logs are for debugging purposes only, so no guarantee of correctness or stability // 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) 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 ctx context.Context
var cancel context.CancelFunc var cancel context.CancelFunc
var finished 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()) 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") 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())
} }

@ -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
}

@ -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
}

@ -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))
}

@ -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"
)

@ -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 RequireSignInView bool
EnableNotifyMail bool EnableNotifyMail bool
EnableBasicAuth bool EnableBasicAuth bool
EnablePasskeyAuth bool
EnableReverseProxyAuth bool EnableReverseProxyAuth bool
EnableReverseProxyAuthAPI bool EnableReverseProxyAuthAPI bool
EnableReverseProxyAutoRegister bool EnableReverseProxyAutoRegister bool
@ -161,6 +162,7 @@ func loadServiceFrom(rootCfg ConfigProvider) {
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool() Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true) Service.EnableBasicAuth = sec.Key("ENABLE_BASIC_AUTHENTICATION").MustBool(true)
Service.EnablePasswordSignInForm = sec.Key("ENABLE_PASSWORD_SIGNIN_FORM").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.EnableReverseProxyAuth = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION").MustBool()
Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool() Service.EnableReverseProxyAuthAPI = sec.Key("ENABLE_REVERSE_PROXY_AUTHENTICATION_API").MustBool()
Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool() Service.EnableReverseProxyAutoRegister = sec.Key("ENABLE_REVERSE_PROXY_AUTO_REGISTRATION").MustBool()

@ -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 ( import (
"context" "context"
"net/http" "net/http"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/reqctx"
) )
type contextKeyType struct{} type contextKeyType struct{}
@ -14,10 +17,12 @@ var contextKey contextKeyType
// RecordFuncInfo records a func info into context // RecordFuncInfo records a func info into context
func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) { func RecordFuncInfo(ctx context.Context, funcInfo *FuncInfo) (end func()) {
// TODO: reqCtx := reqctx.FromContext(ctx), add trace support
end = func() {} end = func() {}
if reqCtx := reqctx.FromContext(ctx); reqCtx != nil {
// save the func info into the context record 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 { if record, ok := ctx.Value(contextKey).(*requestRecord); ok {
record.lock.Lock() record.lock.Lock()
record.funcInfo = funcInfo record.funcInfo = funcInfo

@ -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" HookEventRepository HookEventType = "repository"
HookEventRelease HookEventType = "release" HookEventRelease HookEventType = "release"
HookEventPackage HookEventType = "package" HookEventPackage HookEventType = "package"
HookEventSchedule HookEventType = "schedule"
HookEventStatus HookEventType = "status" 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 // Event returns the HookEventType as an event string
func (h HookEventType) Event() string { func (h HookEventType) Event() string {
switch h { switch h {
case HookEventCreate:
return "create"
case HookEventDelete:
return "delete"
case HookEventFork:
return "fork"
case HookEventPush:
return "push"
case HookEventIssues, HookEventIssueAssign, HookEventIssueLabel, HookEventIssueMilestone: case HookEventIssues, HookEventIssueAssign, HookEventIssueLabel, HookEventIssueMilestone:
return "issues" return "issues"
case HookEventPullRequest, HookEventPullRequestAssign, HookEventPullRequestLabel, HookEventPullRequestMilestone, case HookEventPullRequest, HookEventPullRequestAssign, HookEventPullRequestLabel, HookEventPullRequestMilestone,
@ -59,14 +85,9 @@ func (h HookEventType) Event() string {
return "pull_request_rejected" return "pull_request_rejected"
case HookEventPullRequestReviewComment: case HookEventPullRequestReviewComment:
return "pull_request_comment" return "pull_request_comment"
case HookEventWiki: default:
return "wiki" return string(h)
case HookEventRepository:
return "repository"
case HookEventRelease:
return "release"
} }
return ""
} }
func (h HookEventType) IsPullRequest() bool { 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>. 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ů 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=Přijmout převod
transfer.accept_desc=Převést do „%s“ 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_set=Nastavit odhadovaný čas
issues.time_estimate_display=Odhad: %s 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.remove_time_estimate_at=odstranil/a odhad času %s
issues.time_estimate_invalid=Formát odhadu času je neplatný issues.time_estimate_invalid=Formát odhadu času je neplatný
issues.start_tracking_history=započal/a práci %s issues.start_tracking_history=započal/a práci %s
issues.tracker_auto_close=Časovač se automaticky zastaví po zavření tohoto úkolu 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.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.cancel_tracking_history=`zrušil/a sledování času %s`
issues.del_time=Odstranit tento časový záznam 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.del_time_history=`odstranil/a strávený čas %s`
issues.add_time_manually=Přidat čas ručně issues.add_time_manually=Přidat čas ručně
issues.add_time_hours=Hodiny 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_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_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_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.deleted_branch=(odstraněno):%s
pull.agit_documentation=Prohlédněte si dokumentaci o AGit 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.wiki_desc=Povolit Wiki repozitáře
settings.use_internal_wiki=Používat vestavěnou Wiki settings.use_internal_wiki=Používat vestavěnou Wiki
settings.default_wiki_branch_name=Výchozí název větve 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.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.use_external_wiki=Používat externí Wiki
settings.external_wiki_url=URL 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=Vytvořit novou větev
branch.new_branch_from=Vytvořit novou větev z „%s“ branch.new_branch_from=Vytvořit novou větev z „%s“
branch.renamed=Větev %s byla přejmenována na %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=Vytvořit značku %s
tag.create_tag_operation=Vytvořit značku 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.execute_times=Vykonání
monitor.process=Spuštěné procesy monitor.process=Spuštěné procesy
monitor.stacktrace=Výpisy zásobníku monitor.stacktrace=Výpisy zásobníku
monitor.trace=Trasovat
monitor.performance_logs=Výkonnostní logy
monitor.processes_count=%d procesů monitor.processes_count=%d procesů
monitor.download_diagnosis_report=Stáhnout diagnosttickou zprávu monitor.download_diagnosis_report=Stáhnout diagnosttickou zprávu
monitor.desc=Popis monitor.desc=Popis
@ -3366,7 +3377,6 @@ monitor.execute_time=Doba provádění
monitor.last_execution_result=Výsledek monitor.last_execution_result=Výsledek
monitor.process.cancel=Zrušit proces monitor.process.cancel=Zrušit proces
monitor.process.cancel_desc=Zrušení procesu může způsobit ztrátu dat 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.process.children=Potomek
monitor.queues=Fronty 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.type=Typ obrazu
container.details.platform=Platforma container.details.platform=Platforma
container.pull=Stáhněte obraz z příkazové řádky: 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.multi_arch=OS/architektura
container.layers=Vrstvy obrazů container.layers=Vrstvy obrazů
container.labels=Štítky container.labels=Štítky

@ -3356,7 +3356,6 @@ monitor.execute_time=Ausführungszeit
monitor.last_execution_result=Ergebnis monitor.last_execution_result=Ergebnis
monitor.process.cancel=Prozess abbrechen monitor.process.cancel=Prozess abbrechen
monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen monitor.process.cancel_desc=Abbrechen eines Prozesses kann Datenverlust verursachen
monitor.process.cancel_notices=Abbrechen: <strong>%s</strong>?
monitor.process.children=Subprozesse monitor.process.children=Subprozesse
monitor.queues=Warteschlangen monitor.queues=Warteschlangen

@ -3368,6 +3368,8 @@ monitor.previous = Previous Time
monitor.execute_times = Executions monitor.execute_times = Executions
monitor.process = Running Processes monitor.process = Running Processes
monitor.stacktrace = Stacktrace monitor.stacktrace = Stacktrace
monitor.trace = Trace
monitor.performance_logs = Performance Logs
monitor.processes_count = %d Processes monitor.processes_count = %d Processes
monitor.download_diagnosis_report = Download diagnosis report monitor.download_diagnosis_report = Download diagnosis report
monitor.desc = Description monitor.desc = Description
@ -3376,7 +3378,6 @@ monitor.execute_time = Execution Time
monitor.last_execution_result = Result monitor.last_execution_result = Result
monitor.process.cancel = Cancel process monitor.process.cancel = Cancel process
monitor.process.cancel_desc = Cancelling a process may cause data loss monitor.process.cancel_desc = Cancelling a process may cause data loss
monitor.process.cancel_notices = Cancel: <strong>%s</strong>?
monitor.process.children = Children monitor.process.children = Children
monitor.queues = Queues monitor.queues = Queues

@ -3367,7 +3367,6 @@ monitor.execute_time=Heure d'Éxécution
monitor.last_execution_result=Résultat monitor.last_execution_result=Résultat
monitor.process.cancel=Annuler le processus 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_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.process.children=Enfant
monitor.queues=Files d'attente monitor.queues=Files d'attente

@ -3368,7 +3368,6 @@ monitor.execute_time=Am Forghníomhaithe
monitor.last_execution_result=Toradh monitor.last_execution_result=Toradh
monitor.process.cancel=Cealaigh próiseas 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_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.process.children=Leanaí
monitor.queues=Scuaineanna monitor.queues=Scuaineanna

@ -244,6 +244,7 @@ license_desc=Go get <a target="_blank" rel="noopener noreferrer" href="%[1]s">%[
[install] [install]
install=インストール install=インストール
installing_desc=インストール中です、お待ちください...
title=初期設定 title=初期設定
docker_helper=GiteaをDocker内で実行する場合は、設定を変更する前に<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を読んでください。 docker_helper=GiteaをDocker内で実行する場合は、設定を変更する前に<a target="_blank" rel="noopener noreferrer" href="%s">ドキュメント</a>を読んでください。
require_db_desc=Giteaには、MySQL、PostgreSQL、MSSQL、SQLite3、またはTiDB(MySQL プロトコル) が必要です。 require_db_desc=Giteaには、MySQL、PostgreSQL、MSSQL、SQLite3、またはTiDB(MySQL プロトコル) が必要です。
@ -1015,6 +1016,8 @@ new_repo_helper=リポジトリには、プロジェクトのすべてのファ
owner=オーナー owner=オーナー
owner_helper=リポジトリ数の上限により、一部の組織はドロップダウンに表示されない場合があります。 owner_helper=リポジトリ数の上限により、一部の組織はドロップダウンに表示されない場合があります。
repo_name=リポジトリ名 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_name_helper=リポジトリ名は、短く、覚えやすく、他と重複しないキーワードを使用しましょう。 リポジトリ名を ".profile" または ".profile-private" にして README.md を追加すると、ユーザーや組織のプロフィールとなります。
repo_size=リポジトリサイズ repo_size=リポジトリサイズ
template=テンプレート template=テンプレート
@ -3364,7 +3367,6 @@ monitor.execute_time=実行時間
monitor.last_execution_result=結果 monitor.last_execution_result=結果
monitor.process.cancel=処理をキャンセル monitor.process.cancel=処理をキャンセル
monitor.process.cancel_desc=処理をキャンセルするとデータが失われる可能性があります monitor.process.cancel_desc=処理をキャンセルするとデータが失われる可能性があります
monitor.process.cancel_notices=キャンセル: <strong>%s</strong>?
monitor.process.children=子プロセス monitor.process.children=子プロセス
monitor.queues=キュー monitor.queues=キュー

@ -2310,7 +2310,6 @@ monitor.start=Czas rozpoczęcia
monitor.execute_time=Czas wykonania monitor.execute_time=Czas wykonania
monitor.process.cancel=Anuluj proces monitor.process.cancel=Anuluj proces
monitor.process.cancel_desc=Anulowanie procesu może spowodować utratę danych monitor.process.cancel_desc=Anulowanie procesu może spowodować utratę danych
monitor.process.cancel_notices=Anuluj: <strong>%s</strong>?
monitor.queues=Kolejki monitor.queues=Kolejki
monitor.queue=Kolejka: %s 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_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_prompt_base_newer=O ramo base %s tem novas modificações
pulls.upstream_diverging_merge=Sincronizar derivação 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.deleted_branch=(eliminado):%s
pull.agit_documentation=Rever a documentação sobre o AGit 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.execute_times=Execuções
monitor.process=Processos em execução monitor.process=Processos em execução
monitor.stacktrace=Vestígios da pilha monitor.stacktrace=Vestígios da pilha
monitor.trace=Rastreio
monitor.performance_logs=Registos de desempenho
monitor.processes_count=%d processos monitor.processes_count=%d processos
monitor.download_diagnosis_report=Descarregar relatório de diagnóstico monitor.download_diagnosis_report=Descarregar relatório de diagnóstico
monitor.desc=Descrição monitor.desc=Descrição
@ -3374,7 +3377,6 @@ monitor.execute_time=Tempo de execução
monitor.last_execution_result=Resultado monitor.last_execution_result=Resultado
monitor.process.cancel=Cancelar processo monitor.process.cancel=Cancelar processo
monitor.process.cancel_desc=Cancelar um processo pode resultar na perda de dados 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.process.children=Descendentes
monitor.queues=Filas monitor.queues=Filas

@ -3356,7 +3356,6 @@ monitor.execute_time=执行时长
monitor.last_execution_result=结果 monitor.last_execution_result=结果
monitor.process.cancel=中止进程 monitor.process.cancel=中止进程
monitor.process.cancel_desc=中止一个进程可能导致数据丢失 monitor.process.cancel_desc=中止一个进程可能导致数据丢失
monitor.process.cancel_notices=中止:<strong>%s</strong> ?
monitor.process.children=子进程 monitor.process.children=子进程
monitor.queues=队列 monitor.queues=队列

@ -3347,7 +3347,6 @@ monitor.execute_time=已執行時間
monitor.last_execution_result=結果 monitor.last_execution_result=結果
monitor.process.cancel=結束處理程序 monitor.process.cancel=結束處理程序
monitor.process.cancel_desc=結束處理程序可能造成資料遺失 monitor.process.cancel_desc=結束處理程序可能造成資料遺失
monitor.process.cancel_notices=結束: <strong>%s</strong>?
monitor.process.children=子程序 monitor.process.children=子程序
monitor.queues=佇列 monitor.queues=佇列

988
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -32,7 +32,7 @@
"htmx.org": "2.0.4", "htmx.org": "2.0.4",
"idiomorph": "0.4.0", "idiomorph": "0.4.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"katex": "0.16.20", "katex": "0.16.21",
"license-checker-webpack-plugin": "0.2.1", "license-checker-webpack-plugin": "0.2.1",
"mermaid": "11.4.1", "mermaid": "11.4.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
@ -79,8 +79,8 @@
"@types/throttle-debounce": "5.0.2", "@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6", "@types/tinycolor2": "1.4.6",
"@types/toastify-js": "1.12.3", "@types/toastify-js": "1.12.3",
"@typescript-eslint/eslint-plugin": "8.20.0", "@typescript-eslint/eslint-plugin": "8.21.0",
"@typescript-eslint/parser": "8.20.0", "@typescript-eslint/parser": "8.21.0",
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.1",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-import-resolver-typescript": "3.7.0", "eslint-import-resolver-typescript": "3.7.0",
@ -98,7 +98,7 @@
"eslint-plugin-vue": "9.32.0", "eslint-plugin-vue": "9.32.0",
"eslint-plugin-vue-scoped-css": "2.9.0", "eslint-plugin-vue-scoped-css": "2.9.0",
"eslint-plugin-wc": "2.2.0", "eslint-plugin-wc": "2.2.0",
"happy-dom": "16.6.0", "happy-dom": "16.7.2",
"markdownlint-cli": "0.43.0", "markdownlint-cli": "0.43.0",
"nolyfill": "1.0.43", "nolyfill": "1.0.43",
"postcss-html": "1.8.0", "postcss-html": "1.8.0",
@ -107,10 +107,10 @@
"stylelint-declaration-strict-value": "1.10.7", "stylelint-declaration-strict-value": "1.10.7",
"stylelint-value-no-unknown-custom-properties": "6.0.1", "stylelint-value-no-unknown-custom-properties": "6.0.1",
"svgo": "3.3.2", "svgo": "3.3.2",
"type-fest": "4.32.0", "type-fest": "4.33.0",
"updates": "16.4.1", "updates": "16.4.1",
"vite-string-plugin": "1.3.4", "vite-string-plugin": "1.4.3",
"vitest": "2.1.8", "vitest": "3.0.3",
"vue-tsc": "2.2.0" "vue-tsc": "2.2.0"
}, },
"browserslist": [ "browserslist": [

@ -23,7 +23,7 @@ func TestTestHook(t *testing.T) {
contexttest.LoadRepoCommit(t, ctx) contexttest.LoadRepoCommit(t, ctx)
contexttest.LoadUser(t, ctx, 2) contexttest.LoadUser(t, ctx, 2)
TestHook(ctx) TestHook(ctx)
assert.EqualValues(t, http.StatusNoContent, ctx.Resp.Status()) assert.EqualValues(t, http.StatusNoContent, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{ unittest.AssertExistsAndLoadBean(t, &webhook.HookTask{
HookID: 1, HookID: 1,

@ -58,7 +58,7 @@ func TestRepoEdit(t *testing.T) {
web.SetForm(ctx, &opts) web.SetForm(ctx, &opts)
Edit(ctx) Edit(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
ID: 1, ID: 1,
}, unittest.Cond("name = ? AND is_archived = 1", *opts.Name)) }, unittest.Cond("name = ? AND is_archived = 1", *opts.Name))
@ -78,7 +78,7 @@ func TestRepoEditNameChange(t *testing.T) {
web.SetForm(ctx, &opts) web.SetForm(ctx, &opts)
Edit(ctx) Edit(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{
ID: 1, ID: 1,

@ -185,26 +185,27 @@ func addHook(ctx *context.APIContext, form *api.CreateHookOption, ownerID, repoI
HookEvent: &webhook_module.HookEvent{ HookEvent: &webhook_module.HookEvent{
ChooseEvents: true, ChooseEvents: true,
HookEvents: webhook_module.HookEvents{ HookEvents: webhook_module.HookEvents{
Create: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true), webhook_module.HookEventCreate: util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true),
Delete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true), webhook_module.HookEventDelete: util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true),
Fork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true), webhook_module.HookEventFork: util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true),
Issues: issuesHook(form.Events, "issues_only"), webhook_module.HookEventIssues: issuesHook(form.Events, "issues_only"),
IssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)), webhook_module.HookEventIssueAssign: issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)),
IssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)), webhook_module.HookEventIssueLabel: issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)),
IssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)), webhook_module.HookEventIssueMilestone: issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)),
IssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)), webhook_module.HookEventIssueComment: issuesHook(form.Events, string(webhook_module.HookEventIssueComment)),
Push: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true), webhook_module.HookEventPush: util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true),
PullRequest: pullHook(form.Events, "pull_request_only"), webhook_module.HookEventPullRequest: pullHook(form.Events, "pull_request_only"),
PullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)), webhook_module.HookEventPullRequestAssign: pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)),
PullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)), webhook_module.HookEventPullRequestLabel: pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)),
PullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)), webhook_module.HookEventPullRequestMilestone: pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)),
PullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)), webhook_module.HookEventPullRequestComment: pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)),
PullRequestReview: pullHook(form.Events, "pull_request_review"), webhook_module.HookEventPullRequestReview: pullHook(form.Events, "pull_request_review"),
PullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)), webhook_module.HookEventPullRequestReviewRequest: pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)),
PullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)), webhook_module.HookEventPullRequestSync: pullHook(form.Events, string(webhook_module.HookEventPullRequestSync)),
Wiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true), webhook_module.HookEventWiki: util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true),
Repository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true), webhook_module.HookEventRepository: util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true),
Release: util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), 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, BranchFilter: form.BranchFilter,
}, },
@ -356,14 +357,13 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
w.PushOnly = false w.PushOnly = false
w.SendEverything = false w.SendEverything = false
w.ChooseEvents = true w.ChooseEvents = true
w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true) w.HookEvents[webhook_module.HookEventCreate] = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true)
w.Push = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true) w.HookEvents[webhook_module.HookEventPush] = util.SliceContainsString(form.Events, string(webhook_module.HookEventPush), true)
w.Create = util.SliceContainsString(form.Events, string(webhook_module.HookEventCreate), true) w.HookEvents[webhook_module.HookEventDelete] = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true)
w.Delete = util.SliceContainsString(form.Events, string(webhook_module.HookEventDelete), true) w.HookEvents[webhook_module.HookEventFork] = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true)
w.Fork = util.SliceContainsString(form.Events, string(webhook_module.HookEventFork), true) w.HookEvents[webhook_module.HookEventRepository] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true)
w.Repository = util.SliceContainsString(form.Events, string(webhook_module.HookEventRepository), true) w.HookEvents[webhook_module.HookEventWiki] = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true)
w.Wiki = util.SliceContainsString(form.Events, string(webhook_module.HookEventWiki), true) w.HookEvents[webhook_module.HookEventRelease] = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true)
w.Release = util.SliceContainsString(form.Events, string(webhook_module.HookEventRelease), true)
w.BranchFilter = form.BranchFilter w.BranchFilter = form.BranchFilter
err := w.SetHeaderAuthorization(form.AuthorizationHeader) err := w.SetHeaderAuthorization(form.AuthorizationHeader)
@ -373,21 +373,20 @@ func editHook(ctx *context.APIContext, form *api.EditHookOption, w *webhook.Webh
} }
// Issues // Issues
w.Issues = issuesHook(form.Events, "issues_only") w.HookEvents[webhook_module.HookEventIssues] = issuesHook(form.Events, "issues_only")
w.IssueAssign = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign)) w.HookEvents[webhook_module.HookEventIssueAssign] = issuesHook(form.Events, string(webhook_module.HookEventIssueAssign))
w.IssueLabel = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel)) w.HookEvents[webhook_module.HookEventIssueLabel] = issuesHook(form.Events, string(webhook_module.HookEventIssueLabel))
w.IssueMilestone = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone)) w.HookEvents[webhook_module.HookEventIssueMilestone] = issuesHook(form.Events, string(webhook_module.HookEventIssueMilestone))
w.IssueComment = issuesHook(form.Events, string(webhook_module.HookEventIssueComment)) w.HookEvents[webhook_module.HookEventIssueComment] = issuesHook(form.Events, string(webhook_module.HookEventIssueComment))
// Pull requests // Pull requests
w.PullRequest = pullHook(form.Events, "pull_request_only") w.HookEvents[webhook_module.HookEventPullRequest] = pullHook(form.Events, "pull_request_only")
w.PullRequestAssign = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign)) w.HookEvents[webhook_module.HookEventPullRequestAssign] = pullHook(form.Events, string(webhook_module.HookEventPullRequestAssign))
w.PullRequestLabel = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel)) w.HookEvents[webhook_module.HookEventPullRequestLabel] = pullHook(form.Events, string(webhook_module.HookEventPullRequestLabel))
w.PullRequestMilestone = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone)) w.HookEvents[webhook_module.HookEventPullRequestMilestone] = pullHook(form.Events, string(webhook_module.HookEventPullRequestMilestone))
w.PullRequestComment = pullHook(form.Events, string(webhook_module.HookEventPullRequestComment)) w.HookEvents[webhook_module.HookEventPullRequestReview] = pullHook(form.Events, "pull_request_review")
w.PullRequestReview = pullHook(form.Events, "pull_request_review") w.HookEvents[webhook_module.HookEventPullRequestReviewRequest] = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest))
w.PullRequestReviewRequest = pullHook(form.Events, string(webhook_module.HookEventPullRequestReviewRequest)) w.HookEvents[webhook_module.HookEventPullRequestSync] = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync))
w.PullRequestSync = pullHook(form.Events, string(webhook_module.HookEventPullRequestSync))
if err := w.UpdateEvent(); err != nil { if err := w.UpdateEvent(); err != nil {
ctx.Error(http.StatusInternalServerError, "UpdateEvent", err) ctx.Error(http.StatusInternalServerError, "UpdateEvent", err)

@ -9,6 +9,7 @@ import (
"strings" "strings"
"code.gitea.io/gitea/modules/cache" "code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/gtprof"
"code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/reqctx" "code.gitea.io/gitea/modules/reqctx"
"code.gitea.io/gitea/modules/setting" "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) ctx, finished := reqctx.NewRequestContext(req.Context(), profDesc)
defer finished() 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() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
RenderPanicErrorPage(respWriter, req, err) // it should never panic 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 // make sure chi uses EscapedPath(RawPath) as RoutePath, then "%2f" could be handled correctly
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
ctx := chi.RouteContext(req.Context()) chiCtx := chi.RouteContext(req.Context())
if req.URL.RawPath == "" { if req.URL.RawPath == "" {
ctx.RoutePath = req.URL.EscapedPath() chiCtx.RoutePath = req.URL.EscapedPath()
} else { } else {
ctx.RoutePath = req.URL.RawPath chiCtx.RoutePath = req.URL.RawPath
} }
next.ServeHTTP(resp, req) next.ServeHTTP(resp, req)
}) })

@ -37,6 +37,7 @@ const (
tplSelfCheck templates.TplName = "admin/self_check" tplSelfCheck templates.TplName = "admin/self_check"
tplCron templates.TplName = "admin/cron" tplCron templates.TplName = "admin/cron"
tplQueue templates.TplName = "admin/queue" tplQueue templates.TplName = "admin/queue"
tplPerfTrace templates.TplName = "admin/perftrace"
tplStacktrace templates.TplName = "admin/stacktrace" tplStacktrace templates.TplName = "admin/stacktrace"
tplQueueManage templates.TplName = "admin/queue_manage" tplQueueManage templates.TplName = "admin/queue_manage"
tplStats templates.TplName = "admin/stats" tplStats templates.TplName = "admin/stats"

@ -10,13 +10,15 @@ import (
"time" "time"
"code.gitea.io/gitea/modules/httplib" "code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/tailmsg"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/context"
) )
func MonitorDiagnosis(ctx *context.Context) { func MonitorDiagnosis(ctx *context.Context) {
seconds := ctx.FormInt64("seconds") seconds := ctx.FormInt64("seconds")
if seconds <= 5 { if seconds <= 1 {
seconds = 5 seconds = 1
} }
if seconds > 300 { if seconds > 300 {
seconds = 300 seconds = 300
@ -65,4 +67,16 @@ func MonitorDiagnosis(ctx *context.Context) {
return return
} }
_ = pprof.Lookup("heap").WriteTo(f, 0) _ = 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"))
}
} }

@ -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" "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 // Stacktrace show admin monitor goroutines page
func Stacktrace(ctx *context.Context) { func Stacktrace(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("admin.monitor") monitorTraceCommon(ctx)
ctx.Data["PageIsAdminMonitorStacktrace"] = true
ctx.Data["GoroutineCount"] = runtime.NumGoroutine() ctx.Data["GoroutineCount"] = runtime.NumGoroutine()

@ -169,6 +169,7 @@ func prepareSignInPageData(ctx *context.Context) {
ctx.Data["PageIsLogin"] = true ctx.Data["PageIsLogin"] = true
ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx) ctx.Data["EnableSSPI"] = auth.IsSSPIEnabled(ctx)
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["EnablePasskeyAuth"] = setting.Service.EnablePasskeyAuth
if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin { if setting.Service.EnableCaptcha && setting.Service.RequireCaptchaForLogin {
context.SetCaptchaData(ctx) context.SetCaptchaData(ctx)

@ -46,6 +46,7 @@ func LinkAccount(ctx *context.Context) {
ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false 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 // 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" 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["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false 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 // 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" 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["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration
ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm ctx.Data["EnablePasswordSignInForm"] = setting.Service.EnablePasswordSignInForm
ctx.Data["ShowRegistrationButton"] = false 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 // 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" 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 // WebAuthnPasskeyAssertion submits a WebAuthn challenge for the passkey login to the browser
func WebAuthnPasskeyAssertion(ctx *context.Context) { func WebAuthnPasskeyAssertion(ctx *context.Context) {
if !setting.Service.EnablePasskeyAuth {
ctx.Error(http.StatusForbidden)
return
}
assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin() assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin()
if err != nil { if err != nil {
ctx.ServerError("webauthn.BeginDiscoverableLogin", err) ctx.ServerError("webauthn.BeginDiscoverableLogin", err)
@ -66,6 +71,11 @@ func WebAuthnPasskeyAssertion(ctx *context.Context) {
// WebAuthnPasskeyLogin handles the WebAuthn login process using a Passkey // WebAuthnPasskeyLogin handles the WebAuthn login process using a Passkey
func WebAuthnPasskeyLogin(ctx *context.Context) { func WebAuthnPasskeyLogin(ctx *context.Context) {
if !setting.Service.EnablePasskeyAuth {
ctx.Error(http.StatusForbidden)
return
}
sessionData, okData := ctx.Session.Get("webauthnPasskeyAssertion").(*webauthn.SessionData) sessionData, okData := ctx.Session.Get("webauthnPasskeyAssertion").(*webauthn.SessionData)
if !okData || sessionData == nil { if !okData || sessionData == nil {
ctx.ServerError("ctx.Session.Get", errors.New("not in WebAuthn session")) 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) contexttest.LoadRepo(t, ctx, 2)
web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"}) web.SetForm(ctx, &forms.InitializeLabelsForm{TemplateName: "Default"})
InitializeLabels(ctx) InitializeLabels(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
RepoID: 2, RepoID: 2,
Name: "enhancement", Name: "enhancement",
@ -84,7 +84,7 @@ func TestNewLabel(t *testing.T) {
Color: "#abcdef", Color: "#abcdef",
}) })
NewLabel(ctx) NewLabel(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
Name: "newlabel", Name: "newlabel",
Color: "#abcdef", Color: "#abcdef",
@ -104,7 +104,7 @@ func TestUpdateLabel(t *testing.T) {
IsArchived: true, IsArchived: true,
}) })
UpdateLabel(ctx) UpdateLabel(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &issues_model.Label{ unittest.AssertExistsAndLoadBean(t, &issues_model.Label{
ID: 2, ID: 2,
Name: "newnameforlabel", Name: "newnameforlabel",
@ -120,7 +120,7 @@ func TestDeleteLabel(t *testing.T) {
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
ctx.Req.Form.Set("id", "2") ctx.Req.Form.Set("id", "2")
DeleteLabel(ctx) 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.Label{ID: 2})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2}) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{LabelID: 2})
assert.EqualValues(t, ctx.Tr("repo.issues.label_deletion_success"), ctx.Flash.SuccessMsg) 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("issue_ids", "1,3")
ctx.Req.Form.Set("action", "clear") ctx.Req.Form.Set("action", "clear")
UpdateIssueLabel(ctx) 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: 1})
unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 3}) unittest.AssertNotExistsBean(t, &issues_model.IssueLabel{IssueID: 3})
unittest.CheckConsistencyFor(t, &issues_model.Label{}) 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("action", testCase.Action)
ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID))) ctx.Req.Form.Set("id", strconv.Itoa(int(testCase.LabelID)))
UpdateIssueLabel(ctx) UpdateIssueLabel(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
for _, issueID := range testCase.IssueIDs { for _, issueID := range testCase.IssueIDs {
if testCase.ExpectedAdd { if testCase.ExpectedAdd {
unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID}) unittest.AssertExistsAndLoadBean(t, &issues_model.IssueLabel{IssueID: issueID, LabelID: testCase.LabelID})

@ -54,7 +54,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
} }
web.SetForm(ctx, &addKeyForm) web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx) DeployKeysPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{ unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title, Name: addKeyForm.Title,
@ -84,7 +84,7 @@ func TestAddReadWriteOnlyDeployKey(t *testing.T) {
} }
web.SetForm(ctx, &addKeyForm) web.SetForm(ctx, &addKeyForm)
DeployKeysPost(ctx) DeployKeysPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{ unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
Name: addKeyForm.Title, Name: addKeyForm.Title,
@ -121,7 +121,7 @@ func TestCollaborationPost(t *testing.T) {
CollaborationPost(ctx) 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) exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
assert.NoError(t, err) assert.NoError(t, err)
@ -147,7 +147,7 @@ func TestCollaborationPost_InactiveUser(t *testing.T) {
CollaborationPost(ctx) CollaborationPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg) assert.NotEmpty(t, ctx.Flash.ErrorMsg)
} }
@ -179,7 +179,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
CollaborationPost(ctx) 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) exists, err := repo_model.IsCollaborator(ctx, re.ID, 4)
assert.NoError(t, err) assert.NoError(t, err)
@ -188,7 +188,7 @@ func TestCollaborationPost_AddCollaboratorTwice(t *testing.T) {
// Try adding the same collaborator again // Try adding the same collaborator again
CollaborationPost(ctx) CollaborationPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg) assert.NotEmpty(t, ctx.Flash.ErrorMsg)
} }
@ -210,7 +210,7 @@ func TestCollaborationPost_NonExistentUser(t *testing.T) {
CollaborationPost(ctx) CollaborationPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg) assert.NotEmpty(t, ctx.Flash.ErrorMsg)
} }
@ -250,7 +250,7 @@ func TestAddTeamPost(t *testing.T) {
AddTeamPost(ctx) AddTeamPost(ctx)
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID)) 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) assert.Empty(t, ctx.Flash.ErrorMsg)
} }
@ -290,7 +290,7 @@ func TestAddTeamPost_NotAllowed(t *testing.T) {
AddTeamPost(ctx) AddTeamPost(ctx)
assert.False(t, repo_service.HasRepository(db.DefaultContext, team, re.ID)) 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) assert.NotEmpty(t, ctx.Flash.ErrorMsg)
} }
@ -331,7 +331,7 @@ func TestAddTeamPost_AddTeamTwice(t *testing.T) {
AddTeamPost(ctx) AddTeamPost(ctx)
assert.True(t, repo_service.HasRepository(db.DefaultContext, team, re.ID)) 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) assert.NotEmpty(t, ctx.Flash.ErrorMsg)
} }
@ -364,7 +364,7 @@ func TestAddTeamPost_NonExistentTeam(t *testing.T) {
ctx.Repo = repo ctx.Repo = repo
AddTeamPost(ctx) AddTeamPost(ctx)
assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.WrittenStatus())
assert.NotEmpty(t, ctx.Flash.ErrorMsg) assert.NotEmpty(t, ctx.Flash.ErrorMsg)
} }

@ -163,27 +163,27 @@ func ParseHookEvent(form forms.WebhookForm) *webhook_module.HookEvent {
SendEverything: form.SendEverything(), SendEverything: form.SendEverything(),
ChooseEvents: form.ChooseEvents(), ChooseEvents: form.ChooseEvents(),
HookEvents: webhook_module.HookEvents{ HookEvents: webhook_module.HookEvents{
Create: form.Create, webhook_module.HookEventCreate: form.Create,
Delete: form.Delete, webhook_module.HookEventDelete: form.Delete,
Fork: form.Fork, webhook_module.HookEventFork: form.Fork,
Issues: form.Issues, webhook_module.HookEventIssues: form.Issues,
IssueAssign: form.IssueAssign, webhook_module.HookEventIssueAssign: form.IssueAssign,
IssueLabel: form.IssueLabel, webhook_module.HookEventIssueLabel: form.IssueLabel,
IssueMilestone: form.IssueMilestone, webhook_module.HookEventIssueMilestone: form.IssueMilestone,
IssueComment: form.IssueComment, webhook_module.HookEventIssueComment: form.IssueComment,
Release: form.Release, webhook_module.HookEventRelease: form.Release,
Push: form.Push, webhook_module.HookEventPush: form.Push,
PullRequest: form.PullRequest, webhook_module.HookEventPullRequest: form.PullRequest,
PullRequestAssign: form.PullRequestAssign, webhook_module.HookEventPullRequestAssign: form.PullRequestAssign,
PullRequestLabel: form.PullRequestLabel, webhook_module.HookEventPullRequestLabel: form.PullRequestLabel,
PullRequestMilestone: form.PullRequestMilestone, webhook_module.HookEventPullRequestMilestone: form.PullRequestMilestone,
PullRequestComment: form.PullRequestComment, webhook_module.HookEventPullRequestComment: form.PullRequestComment,
PullRequestReview: form.PullRequestReview, webhook_module.HookEventPullRequestReview: form.PullRequestReview,
PullRequestSync: form.PullRequestSync, webhook_module.HookEventPullRequestSync: form.PullRequestSync,
PullRequestReviewRequest: form.PullRequestReviewRequest, webhook_module.HookEventPullRequestReviewRequest: form.PullRequestReviewRequest,
Wiki: form.Wiki, webhook_module.HookEventWiki: form.Wiki,
Repository: form.Repository, webhook_module.HookEventRepository: form.Repository,
Package: form.Package, webhook_module.HookEventPackage: form.Package,
}, },
BranchFilter: form.BranchFilter, BranchFilter: form.BranchFilter,
} }

@ -82,7 +82,7 @@ func TestWiki(t *testing.T) {
ctx.SetPathParam("*", "Home") ctx.SetPathParam("*", "Home")
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
Wiki(ctx) 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"]) assert.EqualValues(t, "Home", ctx.Data["Title"])
assertPagesMetas(t, []string{"Home", "Page With Image", "Page With Spaced Name", "Unescaped File"}, ctx.Data["Pages"]) 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") ctx.SetPathParam("*", "jpeg.jpg")
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
Wiki(ctx) 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")) 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") ctx, _ := contexttest.MockContext(t, "user2/repo1/wiki/?action=_pages")
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
WikiPages(ctx) 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"]) 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.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
NewWiki(ctx) 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"]) assert.EqualValues(t, ctx.Tr("repo.wiki.new_page"), ctx.Data["Title"])
} }
@ -131,7 +131,7 @@ func TestNewWikiPost(t *testing.T) {
Message: message, Message: message,
}) })
NewWikiPost(ctx) 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)) assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
assert.Equal(t, content, wikiContent(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, Message: message,
}) })
NewWikiPost(ctx) 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) assert.EqualValues(t, ctx.Tr("repo.wiki.reserved_page", "_edit"), ctx.Flash.ErrorMsg)
assertWikiNotExists(t, ctx.Repo.Repository, "_edit") assertWikiNotExists(t, ctx.Repo.Repository, "_edit")
} }
@ -162,7 +162,7 @@ func TestEditWiki(t *testing.T) {
contexttest.LoadUser(t, ctx, 2) contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
EditWiki(ctx) 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.EqualValues(t, "Home", ctx.Data["Title"])
assert.Equal(t, wikiContent(t, ctx.Repo.Repository, "Home"), ctx.Data["content"]) 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.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
EditWiki(ctx) EditWiki(ctx)
assert.EqualValues(t, http.StatusForbidden, ctx.Resp.Status()) assert.EqualValues(t, http.StatusForbidden, ctx.Resp.WrittenStatus())
} }
func TestEditWikiPost(t *testing.T) { func TestEditWikiPost(t *testing.T) {
@ -190,7 +190,7 @@ func TestEditWikiPost(t *testing.T) {
Message: message, Message: message,
}) })
EditWikiPost(ctx) 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)) assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))
assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title))) assert.Equal(t, content, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)))
if title != "Home" { if title != "Home" {
@ -206,7 +206,7 @@ func TestDeleteWikiPagePost(t *testing.T) {
contexttest.LoadUser(t, ctx, 2) contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
DeleteWikiPagePost(ctx) DeleteWikiPagePost(ctx)
assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) assert.EqualValues(t, http.StatusOK, ctx.Resp.WrittenStatus())
assertWikiNotExists(t, ctx.Repo.Repository, "Home") assertWikiNotExists(t, ctx.Repo.Repository, "Home")
} }
@ -228,9 +228,9 @@ func TestWikiRaw(t *testing.T) {
contexttest.LoadRepo(t, ctx, 1) contexttest.LoadRepo(t, ctx, 1)
WikiRaw(ctx) WikiRaw(ctx)
if filetype == "" { 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 { } 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) 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. // 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) { func(o *issue_indexer.SearchOptions) {
o.IsFuzzyKeyword = isFuzzy 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 { 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{} ret = &issues_model.IssueStats{}
doerID := ctx.Doer.ID 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() openClosedOpts := opts.Copy()
switch filterMode { 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 })) ret.YourRepositoriesCount, err = issue_indexer.CountIssues(ctx, opts.Copy(func(o *issue_indexer.SearchOptions) { o.AllPublic = false }))
if err != nil { if err != nil {
return nil, err return nil, err

@ -45,7 +45,7 @@ func TestArchivedIssues(t *testing.T) {
Issues(ctx) Issues(ctx)
// Assert: One Issue (ID 30) from one Repo (ID 50) is retrieved, while nothing from archived Repo 51 is retrieved // 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) assert.Len(t, ctx.Data["Issues"], 1)
} }
@ -58,7 +58,7 @@ func TestIssues(t *testing.T) {
contexttest.LoadUser(t, ctx, 2) contexttest.LoadUser(t, ctx, 2)
ctx.Req.Form.Set("state", "closed") ctx.Req.Form.Set("state", "closed")
Issues(ctx) 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.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.Len(t, ctx.Data["Issues"], 1) assert.Len(t, ctx.Data["Issues"], 1)
@ -72,7 +72,7 @@ func TestPulls(t *testing.T) {
contexttest.LoadUser(t, ctx, 2) contexttest.LoadUser(t, ctx, 2)
ctx.Req.Form.Set("state", "open") ctx.Req.Form.Set("state", "open")
Pulls(ctx) 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) 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("state", "closed")
ctx.Req.Form.Set("sort", "furthestduedate") ctx.Req.Form.Set("sort", "furthestduedate")
Milestones(ctx) 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, map[int64]int64{1: 1}, ctx.Data["Counts"])
assert.EqualValues(t, true, ctx.Data["IsShowClosed"]) assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"]) 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("state", "closed")
ctx.Req.Form.Set("sort", "furthestduedate") ctx.Req.Form.Set("sort", "furthestduedate")
Milestones(ctx) 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, map[int64]int64{1: 1}, ctx.Data["Counts"])
assert.EqualValues(t, true, ctx.Data["IsShowClosed"]) assert.EqualValues(t, true, ctx.Data["IsShowClosed"])
assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"]) assert.EqualValues(t, "furthestduedate", ctx.Data["SortType"])

@ -95,7 +95,7 @@ func TestChangePassword(t *testing.T) {
AccountPost(ctx) AccountPost(ctx)
assert.Contains(t, ctx.Flash.ErrorMsg, req.Message) 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.Group("/monitor", func() {
m.Get("/stats", admin.MonitorStats) m.Get("/stats", admin.MonitorStats)
m.Get("/cron", admin.CronTasks) m.Get("/cron", admin.CronTasks)
m.Get("/perftrace", admin.PerfTrace)
m.Get("/stacktrace", admin.Stacktrace) m.Get("/stacktrace", admin.Stacktrace)
m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel) m.Post("/stacktrace/cancel/{pid}", admin.StacktraceCancel)
m.Get("/queue", admin.Queues) m.Get("/queue", admin.Queues)

@ -85,7 +85,7 @@ func (lr *accessLogRecorder) record(start time.Time, respWriter ResponseWriter,
}, },
RequestID: &requestID, RequestID: &requestID,
} }
tmplData.ResponseWriter.Status = respWriter.Status() tmplData.ResponseWriter.Status = respWriter.WrittenStatus()
tmplData.ResponseWriter.Size = respWriter.WrittenSize() tmplData.ResponseWriter.Size = respWriter.WrittenSize()
err = lr.logTemplate.Execute(buf, tmplData) err = lr.logTemplate.Execute(buf, tmplData)
if err != nil { if err != nil {

@ -48,10 +48,6 @@ func (t testAccessLoggerResponseWriterMock) WrittenStatus() int {
return http.StatusOK return http.StatusOK
} }
func (t testAccessLoggerResponseWriterMock) Status() int {
return t.WrittenStatus()
}
func (t testAccessLoggerResponseWriterMock) WrittenSize() int { func (t testAccessLoggerResponseWriterMock) WrittenSize() int {
return 123123 return 123123
} }

@ -11,12 +11,11 @@ import (
// ResponseWriter represents a response writer for HTTP // ResponseWriter represents a response writer for HTTP
type ResponseWriter interface { type ResponseWriter interface {
http.ResponseWriter http.ResponseWriter // provides Header/Write/WriteHeader
http.Flusher http.Flusher // provides Flush
web_types.ResponseStatusProvider web_types.ResponseStatusProvider // provides WrittenStatus
Before(fn func(ResponseWriter)) Before(fn func(ResponseWriter))
Status() int
WrittenSize() 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 // WrittenStatus returned status code written
func (r *Response) WrittenStatus() int { func (r *Response) WrittenStatus() int {
return r.status return r.status

@ -219,26 +219,18 @@ type ProtectBranchPriorityForm struct {
IDs []int64 IDs []int64
} }
// __ __ ___. .__ __
// / \ / \ ____\_ |__ | |__ ____ ____ | | __
// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
// \ /\ ___/| \_\ \ Y ( <_> | <_> ) <
// \__/\ / \___ >___ /___| /\____/ \____/|__|_ \
// \/ \/ \/ \/ \/
// WebhookForm form for changing web hook // WebhookForm form for changing web hook
type WebhookForm struct { type WebhookForm struct {
Events string Events string
Create bool Create bool
Delete bool Delete bool
Fork bool Fork bool
Push bool
Issues bool Issues bool
IssueAssign bool IssueAssign bool
IssueLabel bool IssueLabel bool
IssueMilestone bool IssueMilestone bool
IssueComment bool IssueComment bool
Release bool
Push bool
PullRequest bool PullRequest bool
PullRequestAssign bool PullRequestAssign bool
PullRequestLabel bool PullRequestLabel bool
@ -249,6 +241,7 @@ type WebhookForm struct {
PullRequestReviewRequest bool PullRequestReviewRequest bool
Wiki bool Wiki bool
Repository bool Repository bool
Release bool
Package bool Package bool
Active bool Active bool
BranchFilter string `binding:"GlobPattern"` BranchFilter string `binding:"GlobPattern"`

@ -137,14 +137,8 @@ func PrepareWebhook(ctx context.Context, w *webhook_model.Webhook, event webhook
return nil return nil
} }
for _, e := range w.EventCheckers() { if !w.HasEvent(event) {
if event == e.Type { return nil
if !e.Has() {
return nil
}
break
}
} }
// Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.). // Avoid sending "0 new commits" to non-integration relevant webhooks (e.g. slack, discord, etc.).

@ -95,7 +95,7 @@
<a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/-/admin/notices"> <a class="{{if .PageIsAdminNotices}}active {{end}}item" href="{{AppSubUrl}}/-/admin/notices">
{{ctx.Locale.Tr "admin.notices"}} {{ctx.Locale.Tr "admin.notices"}}
</a> </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> <summary>{{ctx.Locale.Tr "admin.monitor"}}</summary>
<div class="menu"> <div class="menu">
<a class="{{if .PageIsAdminMonitorStats}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stats"> <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"> <a class="{{if .PageIsAdminMonitorQueue}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/queue">
{{ctx.Locale.Tr "admin.monitor.queues"}} {{ctx.Locale.Tr "admin.monitor.queues"}}
</a> </a>
<a class="{{if .PageIsAdminMonitorStacktrace}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stacktrace"> <a class="{{if .PageIsAdminMonitorTrace}}active {{end}}item" href="{{AppSubUrl}}/-/admin/monitor/stacktrace">
{{ctx.Locale.Tr "admin.monitor.stacktrace"}} {{ctx.Locale.Tr "admin.monitor.trace"}}
</a> </a>
</div> </div>
</details> </details>

@ -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>
<div> <div>
{{if or (eq .Process.Type "request") (eq .Process.Type "normal")}} {{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}} {{end}}
</div> </div>
</div> </div>

@ -1,22 +1,7 @@
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}} {{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}}
<div class="admin-setting-content"> <div class="admin-setting-content">
<div class="tw-flex tw-items-center"> {{template "admin/trace_tabs" .}}
<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>
<h4 class="ui top attached header"> <h4 class="ui top attached header">
{{printf "%d Goroutines" .GoroutineCount}}{{/* Goroutine is non-translatable*/}} {{printf "%d Goroutines" .GoroutineCount}}{{/* Goroutine is non-translatable*/}}
@ -34,15 +19,4 @@
{{end}} {{end}}
</div> </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" .}} {{template "admin/layout_footer" .}}

@ -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"> <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"}} {{svg "octicon-triangle-down" 14 "dropdown icon"}}
</button> </button>
<div class="clone-panel-popup tippy-target"> <div class="clone-panel-popup tippy-target">

@ -31,7 +31,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_create"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_create_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_create_desc"}}</span>
</div> </div>
@ -41,7 +41,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_delete"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_delete_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_delete_desc"}}</span>
</div> </div>
@ -51,7 +51,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_fork"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_fork_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_fork_desc"}}</span>
</div> </div>
@ -61,7 +61,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_push"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_push_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_push_desc"}}</span>
</div> </div>
@ -71,7 +71,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_repository"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_repository_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_repository_desc"}}</span>
</div> </div>
@ -81,7 +81,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_release"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_release_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_release_desc"}}</span>
</div> </div>
@ -91,7 +91,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_package"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_package_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_package_desc"}}</span>
</div> </div>
@ -102,7 +102,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_wiki"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_wiki_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_wiki_desc"}}</span>
</div> </div>
@ -117,7 +117,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_issues"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_issues_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_issues_desc"}}</span>
</div> </div>
@ -127,7 +127,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_issue_assign"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_issue_assign_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_issue_assign_desc"}}</span>
</div> </div>
@ -137,7 +137,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_issue_label"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_issue_label_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_issue_label_desc"}}</span>
</div> </div>
@ -147,7 +147,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_issue_milestone"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_issue_milestone_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_issue_milestone_desc"}}</span>
</div> </div>
@ -157,7 +157,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_issue_comment"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_issue_comment_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_issue_comment_desc"}}</span>
</div> </div>
@ -172,7 +172,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <label>{{ctx.Locale.Tr "repo.settings.event_pull_request"}}</label>
<span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_desc"}}</span> <span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_desc"}}</span>
</div> </div>
@ -182,7 +182,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <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> <span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_assign_desc"}}</span>
</div> </div>
@ -192,7 +192,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <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> <span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_label_desc"}}</span>
</div> </div>
@ -202,7 +202,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <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> <span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_milestone_desc"}}</span>
</div> </div>
@ -212,7 +212,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <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> <span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_comment_desc"}}</span>
</div> </div>
@ -222,7 +222,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <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> <span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_review_desc"}}</span>
</div> </div>
@ -232,7 +232,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <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> <span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_sync_desc"}}</span>
</div> </div>
@ -242,7 +242,7 @@
<div class="seven wide column"> <div class="seven wide column">
<div class="field"> <div class="field">
<div class="ui checkbox"> <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> <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> <span class="help">{{ctx.Locale.Tr "repo.settings.event_pull_request_review_request_desc"}}</span>
</div> </div>

@ -60,10 +60,11 @@
</div> </div>
<div class="ui container fluid"> <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"> <div class="ui attached segment header top tw-max-w-2xl tw-m-auto tw-flex tw-flex-col tw-items-center">
<a class="signin-passkey">{{ctx.Locale.Tr "auth.signin_passkey"}}</a> {{if .EnablePasskeyAuth}}
{{template "user/auth/webauthn_error" .}}
<a class="signin-passkey">{{ctx.Locale.Tr "auth.signin_passkey"}}</a>
{{end}}
{{if .ShowRegistrationButton}} {{if .ShowRegistrationButton}}
<div class="field"> <div class="field">

@ -1,12 +1,13 @@
import {expect} from '@playwright/test'; import {expect} from '@playwright/test';
import {env} from 'node:process'; import {env} from 'node:process';
import type {Browser, Page, WorkerInfo} from '@playwright/test';
const ARTIFACTS_PATH = `tests/e2e/test-artifacts`; const ARTIFACTS_PATH = `tests/e2e/test-artifacts`;
const LOGIN_PASSWORD = 'password'; const LOGIN_PASSWORD = 'password';
// log in user and store session info. This should generally be // log in user and store session info. This should generally be
// run in test.beforeAll(), then the session can be loaded in tests. // 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 // Set up a new context
const context = await browser.newContext(); const context = await browser.newContext();
const page = await context.newPage(); const page = await context.newPage();
@ -17,8 +18,8 @@ export async function login_user(browser, workerInfo, user) {
expect(response?.status()).toBe(200); // Status OK expect(response?.status()).toBe(200); // Status OK
// Fill out form // Fill out form
await page.type('input[name=user_name]', user); await page.locator('input[name=user_name]').fill(user);
await page.type('input[name=password]', LOGIN_PASSWORD); await page.locator('input[name=password]').fill(LOGIN_PASSWORD);
await page.click('form button.ui.primary.button:visible'); await page.click('form button.ui.primary.button:visible');
await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle
@ -31,7 +32,7 @@ export async function login_user(browser, workerInfo, user) {
return context; 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; let context;
try { try {
context = await browser.newContext({storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`}); 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; return context;
} }
export async function save_visual(page) { export async function save_visual(page: Page) {
// Optionally include visual testing // Optionally include visual testing
if (env.VISUAL_TEST) { if (env.VISUAL_TEST) {
await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle 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) session.MakeRequest(t, req, http.StatusOK)
} }
func TestEnablePasswordSignInForm(t *testing.T) { func TestEnablePasswordSignInFormAndEnablePasskeyAuth(t *testing.T) {
defer tests.PrepareTestEnv(t)() defer tests.PrepareTestEnv(t)()
mockLinkAccount := func(ctx *context.Context) { mockLinkAccount := func(ctx *context.Context) {
@ -141,4 +141,22 @@ func TestEnablePasswordSignInForm(t *testing.T) {
resp = MakeRequest(t, req, http.StatusOK) resp = MakeRequest(t, req, http.StatusOK)
NewHTMLParser(t, resp.Body).AssertElement(t, "form[action='/user/link_account_signin']", true) 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, "stripInternal": true,
"strict": false, "strict": false,
"strictFunctionTypes": true, "strictFunctionTypes": true,
"noImplicitAny": true,
"noImplicitThis": true, "noImplicitThis": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,

@ -130,12 +130,12 @@ export default defineComponent({
}, },
methods: { methods: {
changeTab(t) { changeTab(tab: string) {
this.tab = t; this.tab = tab;
this.updateHistory(); this.updateHistory();
}, },
changeReposFilter(filter) { changeReposFilter(filter: string) {
this.reposFilter = filter; this.reposFilter = filter;
this.repos = []; this.repos = [];
this.page = 1; this.page = 1;
@ -218,7 +218,7 @@ export default defineComponent({
this.searchRepos(); this.searchRepos();
}, },
changePage(page) { changePage(page: number) {
this.page = page; this.page = page;
if (this.page > this.finalPage) { if (this.page > this.finalPage) {
this.page = this.finalPage; this.page = this.finalPage;
@ -256,7 +256,7 @@ export default defineComponent({
} }
if (searchedURL === this.searchURL) { if (searchedURL === this.searchURL) {
this.repos = json.data.map((webSearchRepo) => { this.repos = json.data.map((webSearchRepo: any) => {
return { return {
...webSearchRepo.repository, ...webSearchRepo.repository,
latest_commit_status_state: webSearchRepo.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status 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, 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') { if (searchedQuery === '' && searchedMode === '' && this.archivedFilter === 'both') {
this.reposTotalCount = count; this.reposTotalCount = count;
} }
@ -275,7 +275,7 @@ export default defineComponent({
} }
}, },
repoIcon(repo) { repoIcon(repo: any) {
if (repo.fork) { if (repo.fork) {
return 'octicon-repo-forked'; return 'octicon-repo-forked';
} else if (repo.mirror) { } else if (repo.mirror) {
@ -298,7 +298,7 @@ export default defineComponent({
return commitStatus[status].color; return commitStatus[status].color;
}, },
reposFilterKeyControl(e) { reposFilterKeyControl(e: KeyboardEvent) {
switch (e.key) { switch (e.key) {
case 'Enter': case 'Enter':
document.querySelector<HTMLAnchorElement>('.repo-owner-name-list li.active a')?.click(); 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 {GET} from '../modules/fetch.ts';
import {generateAriaId} from '../modules/fomantic/base.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({ export default defineComponent({
components: {SvgIcon}, components: {SvgIcon},
data: () => { data: () => {
@ -16,9 +32,9 @@ export default defineComponent({
locale: { locale: {
filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'), filter_changes_by_commit: el.getAttribute('data-filter_changes_by_commit'),
} as Record<string, string>, } as Record<string, string>,
commits: [], commits: [] as Array<Commit>,
hoverActivated: false, hoverActivated: false,
lastReviewCommitSha: null, lastReviewCommitSha: '',
uniqueIdMenu: generateAriaId(), uniqueIdMenu: generateAriaId(),
uniqueIdShowAll: generateAriaId(), uniqueIdShowAll: generateAriaId(),
}; };
@ -71,7 +87,7 @@ export default defineComponent({
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') { if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
const item = document.activeElement; // try to highlight the selected commits const item = document.activeElement; // try to highlight the selected commits
const commitIdx = item?.matches('.item') ? item.getAttribute('data-commit-idx') : null; 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) { onKeyUp(event: KeyboardEvent) {
@ -87,7 +103,7 @@ export default defineComponent({
} }
} }
}, },
highlight(commit) { highlight(commit: Commit) {
if (!this.hoverActivated) return; if (!this.hoverActivated) return;
const indexSelected = this.commits.findIndex((x) => x.selected); const indexSelected = this.commits.findIndex((x) => x.selected);
const indexCurrentElem = this.commits.findIndex((x) => x.id === commit.id); 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 */ /** Load the commits to show in this dropdown */
async fetchCommits() { async fetchCommits() {
const resp = await GET(`${this.issueLink}/commits/list`); 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) => { this.commits.push(...results.commits.map((x) => {
x.hovered = false; x.hovered = false;
return x; return x;
@ -166,7 +183,7 @@ export default defineComponent({
* the diff from beginning of PR up to the second clicked commit is * the diff from beginning of PR up to the second clicked commit is
* opened * opened
*/ */
commitClickedShift(commit) { commitClickedShift(commit: Commit) {
this.hoverActivated = !this.hoverActivated; this.hoverActivated = !this.hoverActivated;
commit.selected = true; commit.selected = true;
// Second click -> determine our range and open links accordingly // Second click -> determine our range and open links accordingly

@ -18,14 +18,14 @@ function toggleFileList() {
} }
function diffTypeToString(pType: number) { function diffTypeToString(pType: number) {
const diffTypes = { const diffTypes: Record<string, string> = {
1: 'add', '1': 'add',
2: 'modify', '2': 'modify',
3: 'del', '3': 'del',
4: 'rename', '4': 'rename',
5: 'copy', '5': 'copy',
}; };
return diffTypes[pType]; return diffTypes[String(pType)];
} }
function diffStatsWidth(adds: number, dels: number) { function diffStatsWidth(adds: number, dels: number) {

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import DiffFileTreeItem from './DiffFileTreeItem.vue'; import DiffFileTreeItem, {type Item} from './DiffFileTreeItem.vue';
import {loadMoreFiles} from '../features/repo-diff.ts'; import {loadMoreFiles} from '../features/repo-diff.ts';
import {toggleElem} from '../utils/dom.ts'; import {toggleElem} from '../utils/dom.ts';
import {diffTreeStore} from '../modules/stores.ts'; import {diffTreeStore} from '../modules/stores.ts';
@ -11,7 +11,7 @@ const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';
const store = diffTreeStore(); const store = diffTreeStore();
const fileTree = computed(() => { const fileTree = computed(() => {
const result = []; const result: Array<Item> = [];
for (const file of store.files) { for (const file of store.files) {
// Split file into directories // Split file into directories
const splits = file.Name.split('/'); const splits = file.Name.split('/');
@ -24,15 +24,10 @@ const fileTree = computed(() => {
if (index === splits.length) { if (index === splits.length) {
isFile = true; isFile = true;
} }
let newParent = { let newParent: Item = {
name: split, name: split,
children: [], children: [],
isFile, isFile,
} as {
name: string,
children: any[],
isFile: boolean,
file?: any,
}; };
if (isFile === true) { if (isFile === true) {

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import {SvgIcon} from '../svg.ts'; import {SvgIcon, type SvgName} from '../svg.ts';
import {diffTreeStore} from '../modules/stores.ts'; import {diffTreeStore} from '../modules/stores.ts';
import {ref} from 'vue'; import {ref} from 'vue';
@ -11,7 +11,7 @@ type File = {
IsSubmodule: boolean; IsSubmodule: boolean;
} }
type Item = { export type Item = {
name: string; name: string;
isFile: boolean; isFile: boolean;
file?: File; file?: File;
@ -26,14 +26,14 @@ const store = diffTreeStore();
const collapsed = ref(false); const collapsed = ref(false);
function getIconForDiffType(pType: number) { function getIconForDiffType(pType: number) {
const diffTypes = { const diffTypes: Record<string, {name: SvgName, classes: Array<string>}> = {
1: {name: 'octicon-diff-added', classes: ['text', 'green']}, '1': {name: 'octicon-diff-added', classes: ['text', 'green']},
2: {name: 'octicon-diff-modified', classes: ['text', 'yellow']}, '2': {name: 'octicon-diff-modified', classes: ['text', 'yellow']},
3: {name: 'octicon-diff-removed', classes: ['text', 'red']}, '3': {name: 'octicon-diff-removed', classes: ['text', 'red']},
4: {name: 'octicon-diff-renamed', classes: ['text', 'teal']}, '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 '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) { function fileIcon(file: File) {

@ -36,17 +36,17 @@ const forceMerge = computed(() => {
}); });
watch(mergeStyle, (val) => { 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]')) { for (const elem of document.querySelectorAll('[data-pull-merge-style]')) {
toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val); toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
} }
}); });
onMounted(() => { 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; let mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.value.defaultMergeStyle)?.name;
if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e) => e.allowed)?.name; if (!mergeStyle) mergeStyle = mergeForm.value.mergeStyles.find((e: any) => e.allowed)?.name;
switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow); switchMergeStyle(mergeStyle, !mergeForm.value.canMergeNow);
document.addEventListener('mouseup', hideMergeStyleMenu); document.addEventListener('mouseup', hideMergeStyleMenu);

@ -6,6 +6,7 @@ import {createElementFromAttrs, toggleElem} from '../utils/dom.ts';
import {formatDatetime} from '../utils/time.ts'; import {formatDatetime} from '../utils/time.ts';
import {renderAnsi} from '../render/ansi.ts'; import {renderAnsi} from '../render/ansi.ts';
import {POST, DELETE} from '../modules/fetch.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" // 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'; type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked';
@ -24,6 +25,20 @@ type LogLineCommand = {
prefix: string, 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 { function parseLineCommand(line: LogLine): LogLineCommand | null {
for (const prefix of LogLinePrefixesGroup) { for (const prefix of LogLinePrefixesGroup) {
if (line.message.startsWith(prefix)) { if (line.message.startsWith(prefix)) {
@ -77,7 +92,7 @@ export default defineComponent({
default: '', default: '',
}, },
locale: { locale: {
type: Object as PropType<Record<string, string>>, type: Object as PropType<Record<string, any>>,
default: null, default: null,
}, },
}, },
@ -86,10 +101,10 @@ export default defineComponent({
const {autoScroll, expandRunning} = getLocaleStorageOptions(); const {autoScroll, expandRunning} = getLocaleStorageOptions();
return { return {
// internal state // internal state
loadingAbortController: null, loadingAbortController: null as AbortController | null,
intervalID: null, intervalID: null as IntervalId | null,
currentJobStepsStates: [], currentJobStepsStates: [] as Array<Record<string, any>>,
artifacts: [], artifacts: [] as Array<Record<string, any>>,
onHoverRerunIndex: -1, onHoverRerunIndex: -1,
menuVisible: false, menuVisible: false,
isFullScreen: false, isFullScreen: false,
@ -122,7 +137,7 @@ export default defineComponent({
// canRerun: false, // canRerun: false,
// duration: '', // duration: '',
// }, // },
], ] as Array<Job>,
commit: { commit: {
localeCommit: '', localeCommit: '',
localePushedBy: '', localePushedBy: '',
@ -148,7 +163,7 @@ export default defineComponent({
// duration: '', // duration: '',
// status: '', // status: '',
// } // }
], ] as Array<Step>,
}, },
}; };
}, },
@ -194,7 +209,7 @@ export default defineComponent({
// get the job step logs container ('.job-step-logs') // get the job step logs container ('.job-step-logs')
getJobStepLogsContainer(stepIndex: number): HTMLElement { 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` // 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 // begin a log group
beginLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) { 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'}, const elJobLogGroupSummary = createElementFromAttrs('summary', {class: 'job-log-group-summary'},
this.createLogLine(stepIndex, startTime, { this.createLogLine(stepIndex, startTime, {
index: line.index, index: line.index,
@ -223,7 +238,7 @@ export default defineComponent({
}, },
// end a log group // end a log group
endLogGroup(stepIndex: number, startTime: number, line: LogLine, cmd: LogLineCommand) { 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._stepLogsActiveContainer = null;
el.append(this.createLogLine(stepIndex, startTime, { el.append(this.createLogLine(stepIndex, startTime, {
index: line.index, index: line.index,
@ -393,7 +408,7 @@ export default defineComponent({
if (this.menuVisible) this.menuVisible = false; if (this.menuVisible) this.menuVisible = false;
}, },
toggleTimeDisplay(type: string) { toggleTimeDisplay(type: 'seconds' | 'stamp') {
this.timeVisible[`log-time-${type}`] = !this.timeVisible[`log-time-${type}`]; this.timeVisible[`log-time-${type}`] = !this.timeVisible[`log-time-${type}`];
for (const el of (this.$refs.steps as HTMLElement).querySelectorAll(`.log-time-${type}`)) { for (const el of (this.$refs.steps as HTMLElement).querySelectorAll(`.log-time-${type}`)) {
toggleElem(el, this.timeVisible[`log-time-${type}`]); toggleElem(el, this.timeVisible[`log-time-${type}`]);
@ -422,9 +437,10 @@ export default defineComponent({
const selectedLogStep = window.location.hash; const selectedLogStep = window.location.hash;
if (!selectedLogStep) return; if (!selectedLogStep) return;
const [_, step, _line] = selectedLogStep.split('-'); const [_, step, _line] = selectedLogStep.split('-');
if (!this.currentJobStepsStates[step]) return; const stepNum = Number(step);
if (!this.currentJobStepsStates[step].expanded && this.currentJobStepsStates[step].cursor === null) { if (!this.currentJobStepsStates[stepNum]) return;
this.currentJobStepsStates[step].expanded = true; 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 // need to await for load job if the step log is loaded for the first time
// so logline can be selected by querySelector // so logline can be selected by querySelector
await this.loadJob(); await this.loadJob();

@ -1,4 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
// @ts-expect-error - module exports no types
import {VueBarGraph} from 'vue-bar-graph'; import {VueBarGraph} from 'vue-bar-graph';
import {computed, onMounted, ref} from 'vue'; import {computed, onMounted, ref} from 'vue';

@ -157,7 +157,7 @@ export default defineComponent({
// @ts-expect-error - el is unknown type // @ts-expect-error - el is unknown type
return (el && el.length) ? el[0] : null; return (el && el.length) ? el[0] : null;
}, },
keydown(e) { keydown(e: KeyboardEvent) {
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
e.preventDefault(); e.preventDefault();
@ -181,7 +181,7 @@ export default defineComponent({
this.menuVisible = false; this.menuVisible = false;
} }
}, },
handleTabSwitch(selectedTab) { handleTabSwitch(selectedTab: SelectedTab) {
this.selectedTab = selectedTab; this.selectedTab = selectedTab;
this.focusSearchField(); this.focusSearchField();
this.loadTabItems(); this.loadTabItems();

@ -80,10 +80,10 @@ export default defineComponent({
sortedContributors: {} as Record<string, any>, sortedContributors: {} as Record<string, any>,
type: 'commits', type: 'commits',
contributorsStats: {} as Record<string, any>, contributorsStats: {} as Record<string, any>,
xAxisStart: null, xAxisStart: null as number | null,
xAxisEnd: null, xAxisEnd: null as number | null,
xAxisMin: null, xAxisMin: null as number | null,
xAxisMax: null, xAxisMax: null as number | null,
}), }),
mounted() { mounted() {
this.fetchGraphData(); this.fetchGraphData();
@ -99,7 +99,7 @@ export default defineComponent({
}, },
methods: { methods: {
sortContributors() { sortContributors() {
const contributors = this.filterContributorWeeksByDateRange(); const contributors: Record<string, any> = this.filterContributorWeeksByDateRange();
const criteria = `total_${this.type}`; const criteria = `total_${this.type}`;
this.sortedContributors = Object.values(contributors) this.sortedContributors = Object.values(contributors)
.filter((contributor) => contributor[criteria] !== 0) .filter((contributor) => contributor[criteria] !== 0)
@ -158,7 +158,7 @@ export default defineComponent({
}, },
filterContributorWeeksByDateRange() { filterContributorWeeksByDateRange() {
const filteredData = {}; const filteredData: Record<string, any> = {};
const data = this.contributorsStats; const data = this.contributorsStats;
for (const key of Object.keys(data)) { for (const key of Object.keys(data)) {
const user = data[key]; const user = data[key];
@ -196,7 +196,7 @@ export default defineComponent({
// Normally, chartjs handles this automatically, but it will resize the graph when you // 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. // zoom, pan etc. I think resizing the graph makes it harder to compare things visually.
const maxValue = Math.max( 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); const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
if (coefficient % 1 === 0) return maxValue; 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 // 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. // maxY value for each contributors' graph which again makes it harder to compare.
const maxValue = Math.max( 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); const [coefficient, exp] = maxValue.toExponential().split('e').map(Number);
if (coefficient % 1 === 0) return maxValue; if (coefficient % 1 === 0) return maxValue;
@ -232,8 +232,8 @@ export default defineComponent({
}, },
updateOtherCharts({chart}: {chart: Chart}, reset: boolean = false) { updateOtherCharts({chart}: {chart: Chart}, reset: boolean = false) {
const minVal = chart.options.scales.x.min; const minVal = Number(chart.options.scales.x.min);
const maxVal = chart.options.scales.x.max; const maxVal = Number(chart.options.scales.x.max);
if (reset) { if (reset) {
this.xAxisMin = this.xAxisStart; this.xAxisMin = this.xAxisStart;
this.xAxisMax = this.xAxisEnd; this.xAxisMax = this.xAxisEnd;

@ -35,7 +35,7 @@ onUnmounted(() => {
document.querySelector('#scoped-access-submit').removeEventListener('click', onClickSubmit); document.querySelector('#scoped-access-submit').removeEventListener('click', onClickSubmit);
}); });
function onClickSubmit(e) { function onClickSubmit(e: Event) {
e.preventDefault(); e.preventDefault();
const warningEl = document.querySelector('#scoped-access-warning'); const warningEl = document.querySelector('#scoped-access-warning');

@ -90,7 +90,7 @@ export function initAdminCommon(): void {
onOAuth2UseCustomURLChange(applyDefaultValues); onOAuth2UseCustomURLChange(applyDefaultValues);
} }
function onOAuth2UseCustomURLChange(applyDefaultValues) { function onOAuth2UseCustomURLChange(applyDefaultValues: boolean) {
const provider = document.querySelector<HTMLInputElement>('#oauth2_provider').value; const provider = document.querySelector<HTMLInputElement>('#oauth2_provider').value;
hideElem('.oauth2_use_custom_url_field'); hideElem('.oauth2_use_custom_url_field');
for (const input of document.querySelectorAll<HTMLInputElement>('.oauth2_use_custom_url_field input[required]')) { 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) { async function initInputCitationValue(citationCopyApa: HTMLButtonElement, citationCopyBibtex: HTMLButtonElement) {
const [{Cite, plugins}] = await Promise.all([ const [{Cite, plugins}] = await Promise.all([
// @ts-expect-error: module exports no types
import(/* webpackChunkName: "citation-js-core" */'@citation-js/core'), 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'), 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'), 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'), import(/* webpackChunkName: "citation-js-csl" */'@citation-js/plugin-csl'),
]); ]);
const {citationFileContent} = pageData; 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"` // a '.show-panel' element can show a panel, by `data-panel="selector"`
// if it has "toggle" class, it toggles the panel // if it has "toggle" class, it toggles the panel
const el = e.currentTarget; const el = e.currentTarget as HTMLElement;
e.preventDefault(); e.preventDefault();
const sel = el.getAttribute('data-panel'); const sel = el.getAttribute('data-panel');
if (el.classList.contains('toggle')) { 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"` // 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(); e.preventDefault();
let sel = el.getAttribute('data-panel'); let sel = el.getAttribute('data-panel');
if (sel) { if (sel) {
@ -98,13 +98,13 @@ function onHidePanelClick(e) {
} }
sel = el.getAttribute('data-panel-closest'); sel = el.getAttribute('data-panel-closest');
if (sel) { if (sel) {
hideElem(el.parentNode.closest(sel)); hideElem((el.parentNode as HTMLElement).closest(sel));
return; return;
} }
throw new Error('no panel to hide'); // should never happen, otherwise there is a bug in code 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. // 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. // Each "data-modal-{target}" attribute will be filled to target element's value or text-content.
// * First, try to query '#target' // * First, try to query '#target'
@ -112,7 +112,7 @@ function onShowModalClick(e) {
// * Then, try to query '.target' // * Then, try to query '.target'
// * Then, try to query 'target' as HTML tag // * 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. // 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(); e.preventDefault();
const modalSelector = el.getAttribute('data-modal'); const modalSelector = el.getAttribute('data-modal');
const elModal = document.querySelector(modalSelector); const elModal = document.querySelector(modalSelector);
@ -137,9 +137,9 @@ function onShowModalClick(e) {
} }
if (attrTargetAttr) { if (attrTargetAttr) {
attrTarget[camelize(attrTargetAttr)] = attrib.value; (attrTarget as any)[camelize(attrTargetAttr)] = attrib.value;
} else if (attrTarget.matches('input, textarea')) { } 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 { } else {
attrTarget.textContent = attrib.value; // FIXME: it should be more strict here, only handle div/span/p 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; let reqUrl = formActionUrl;
const reqOpt = {method: formMethod.toUpperCase(), body: null}; const reqOpt = {
method: formMethod.toUpperCase(),
body: null as FormData | null,
};
if (formMethod.toLowerCase() === 'get') { if (formMethod.toLowerCase() === 'get') {
const params = new URLSearchParams(); const params = new URLSearchParams();
for (const [key, value] of formData) { for (const [key, value] of formData) {

@ -17,13 +17,13 @@ export function initGlobalEnterQuickSubmit() {
if (e.key !== 'Enter') return; if (e.key !== 'Enter') return;
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey); const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
if (hasCtrlOrMeta && e.target.matches('textarea')) { if (hasCtrlOrMeta && e.target.matches('textarea')) {
if (handleGlobalEnterQuickSubmit(e.target)) { if (handleGlobalEnterQuickSubmit(e.target as HTMLElement)) {
e.preventDefault(); e.preventDefault();
} }
} else if (e.target.matches('input') && !e.target.closest('form')) { } 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 // 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 // eslint-disable-next-line unicorn/no-lonely-if
if (handleGlobalEnterQuickSubmit(e.target)) { if (handleGlobalEnterQuickSubmit(e.target as HTMLElement)) {
e.preventDefault(); e.preventDefault();
} }
} }

@ -29,10 +29,10 @@ let elementIdCounter = 0;
/** /**
* validate if the given textarea is non-empty. * 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. * @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. // 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. // 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) { if (!textarea.value) {
@ -49,16 +49,25 @@ export function validateTextareaNonEmpty(textarea) {
return true; return true;
} }
type Heights = {
minHeight?: string,
height?: string,
maxHeight?: string,
};
type ComboMarkdownEditorOptions = { type ComboMarkdownEditorOptions = {
editorHeights?: {minHeight?: string, height?: string, maxHeight?: string}, editorHeights?: Heights,
easyMDEOptions?: EasyMDE.Options, easyMDEOptions?: EasyMDE.Options,
}; };
type ComboMarkdownEditorTextarea = HTMLTextAreaElement & {_giteaComboMarkdownEditor: any};
type ComboMarkdownEditorContainer = HTMLElement & {_giteaComboMarkdownEditor?: any};
export class ComboMarkdownEditor { export class ComboMarkdownEditor {
static EventEditorContentChanged = EventEditorContentChanged; static EventEditorContentChanged = EventEditorContentChanged;
static EventUploadStateChanged = EventUploadStateChanged; static EventUploadStateChanged = EventUploadStateChanged;
public container : HTMLElement; public container: HTMLElement;
options: ComboMarkdownEditorOptions; options: ComboMarkdownEditorOptions;
@ -70,7 +79,7 @@ export class ComboMarkdownEditor {
easyMDEToolbarActions: any; easyMDEToolbarActions: any;
easyMDEToolbarDefault: any; easyMDEToolbarDefault: any;
textarea: HTMLTextAreaElement & {_giteaComboMarkdownEditor: any}; textarea: ComboMarkdownEditorTextarea;
textareaMarkdownToolbar: HTMLElement; textareaMarkdownToolbar: HTMLElement;
textareaAutosize: any; textareaAutosize: any;
@ -81,7 +90,7 @@ export class ComboMarkdownEditor {
previewUrl: string; previewUrl: string;
previewContext: string; previewContext: string;
constructor(container, options:ComboMarkdownEditorOptions = {}) { constructor(container: ComboMarkdownEditorContainer, options:ComboMarkdownEditorOptions = {}) {
if (container._giteaComboMarkdownEditor) throw new Error('ComboMarkdownEditor already initialized'); if (container._giteaComboMarkdownEditor) throw new Error('ComboMarkdownEditor already initialized');
container._giteaComboMarkdownEditor = this; container._giteaComboMarkdownEditor = this;
this.options = options; this.options = options;
@ -98,7 +107,7 @@ export class ComboMarkdownEditor {
await this.switchToUserPreference(); await this.switchToUserPreference();
} }
applyEditorHeights(el, heights) { applyEditorHeights(el: HTMLElement, heights: Heights) {
if (!heights) return; if (!heights) return;
if (heights.minHeight) el.style.minHeight = heights.minHeight; if (heights.minHeight) el.style.minHeight = heights.minHeight;
if (heights.height) el.style.height = heights.height; 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); this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(easyMde, this);
const processed = []; const processed = [];
for (const action of actions) { for (const action of actions) {
@ -332,21 +341,21 @@ export class ComboMarkdownEditor {
this.easyMDE = new EasyMDE(easyMDEOpt); this.easyMDE = new EasyMDE(easyMDEOpt);
this.easyMDE.codemirror.on('change', () => triggerEditorContentChanged(this.container)); this.easyMDE.codemirror.on('change', () => triggerEditorContentChanged(this.container));
this.easyMDE.codemirror.setOption('extraKeys', { this.easyMDE.codemirror.setOption('extraKeys', {
'Cmd-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()), 'Cmd-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
'Ctrl-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()), 'Ctrl-Enter': (cm: any) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
Enter: (cm) => { Enter: (cm: any) => {
const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
if (!tributeContainer || tributeContainer.style.display === 'none') { if (!tributeContainer || tributeContainer.style.display === 'none') {
cm.execCommand('newlineAndIndent'); cm.execCommand('newlineAndIndent');
} }
}, },
Up: (cm) => { Up: (cm: any) => {
const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
if (!tributeContainer || tributeContainer.style.display === 'none') { if (!tributeContainer || tributeContainer.style.display === 'none') {
return cm.execCommand('goLineUp'); return cm.execCommand('goLineUp');
} }
}, },
Down: (cm) => { Down: (cm: any) => {
const tributeContainer = document.querySelector<HTMLElement>('.tribute-container'); const tributeContainer = document.querySelector<HTMLElement>('.tribute-container');
if (!tributeContainer || tributeContainer.style.display === 'none') { if (!tributeContainer || tributeContainer.style.display === 'none') {
return cm.execCommand('goLineDown'); return cm.execCommand('goLineDown');
@ -354,14 +363,14 @@ export class ComboMarkdownEditor {
}, },
}); });
this.applyEditorHeights(this.container.querySelector('.CodeMirror-scroll'), this.options.editorHeights); 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) { if (this.dropzone) {
initEasyMDEPaste(this.easyMDE, this.dropzone); initEasyMDEPaste(this.easyMDE, this.dropzone);
} }
hideElem(this.textareaMarkdownToolbar); hideElem(this.textareaMarkdownToolbar);
} }
value(v = undefined) { value(v: any = undefined) {
if (v === undefined) { if (v === undefined) {
if (this.easyMDE) { if (this.easyMDE) {
return this.easyMDE.value(); 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) return null;
if (el.length) el = el[0]; if (el.length) el = el[0];
return el._giteaComboMarkdownEditor; return el._giteaComboMarkdownEditor;

@ -1,10 +1,10 @@
export const EventEditorContentChanged = 'ce-editor-content-changed'; export const EventEditorContentChanged = 'ce-editor-content-changed';
export function triggerEditorContentChanged(target) { export function triggerEditorContentChanged(target: HTMLElement) {
target.dispatchEvent(new CustomEvent(EventEditorContentChanged, {bubbles: true})); target.dispatchEvent(new CustomEvent(EventEditorContentChanged, {bubbles: true}));
} }
export function textareaInsertText(textarea, value) { export function textareaInsertText(textarea: HTMLTextAreaElement, value: string) {
const startPos = textarea.selectionStart; const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd; const endPos = textarea.selectionEnd;
textarea.value = textarea.value.substring(0, startPos) + value + textarea.value.substring(endPos); textarea.value = textarea.value.substring(0, startPos) + value + textarea.value.substring(endPos);
@ -20,7 +20,7 @@ type TextareaValueSelection = {
selEnd: number; selEnd: number;
} }
function handleIndentSelection(textarea: HTMLTextAreaElement, e) { function handleIndentSelection(textarea: HTMLTextAreaElement, e: KeyboardEvent) {
const selStart = textarea.selectionStart; const selStart = textarea.selectionStart;
const selEnd = textarea.selectionEnd; const selEnd = textarea.selectionEnd;
if (selEnd === selStart) return; // do not process when no selection if (selEnd === selStart) return; // do not process when no selection
@ -184,8 +184,13 @@ function handleNewline(textarea: HTMLTextAreaElement, e: Event) {
triggerEditorContentChanged(textarea); 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) => { textarea.addEventListener('keydown', (e) => {
if (isTextExpanderShown(textarea)) return;
if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) { if (e.key === 'Tab' && !e.ctrlKey && !e.metaKey && !e.altKey) {
// use Tab/Shift-Tab to indent/unindent the selected lines // use Tab/Shift-Tab to indent/unindent the selected lines
handleIndentSelection(textarea, e); handleIndentSelection(textarea, e);

@ -8,43 +8,46 @@ import {
generateMarkdownLinkForAttachment, generateMarkdownLinkForAttachment,
} from '../dropzone.ts'; } from '../dropzone.ts';
import type CodeMirror from 'codemirror'; import type CodeMirror from 'codemirror';
import type EasyMDE from 'easymde';
import type {DropzoneFile} from 'dropzone';
let uploadIdCounter = 0; let uploadIdCounter = 0;
export const EventUploadStateChanged = 'ce-upload-state-changed'; export const EventUploadStateChanged = 'ce-upload-state-changed';
export function triggerUploadStateChanged(target) { export function triggerUploadStateChanged(target: HTMLElement) {
target.dispatchEvent(new CustomEvent(EventUploadStateChanged, {bubbles: true})); target.dispatchEvent(new CustomEvent(EventUploadStateChanged, {bubbles: true}));
} }
function uploadFile(dropzoneEl, file) { function uploadFile(dropzoneEl: HTMLElement, file: File) {
return new Promise((resolve) => { return new Promise((resolve) => {
const curUploadId = uploadIdCounter++; const curUploadId = uploadIdCounter++;
file._giteaUploadId = curUploadId; (file as any)._giteaUploadId = curUploadId;
const dropzoneInst = dropzoneEl.dropzone; const dropzoneInst = dropzoneEl.dropzone;
const onUploadDone = ({file}) => { const onUploadDone = ({file}: {file: any}) => {
if (file._giteaUploadId === curUploadId) { if (file._giteaUploadId === curUploadId) {
dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone); dropzoneInst.off(DropzoneCustomEventUploadDone, onUploadDone);
resolve(file); resolve(file);
} }
}; };
dropzoneInst.on(DropzoneCustomEventUploadDone, onUploadDone); 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 { class TextareaEditor {
editor : HTMLTextAreaElement; editor: HTMLTextAreaElement;
constructor(editor) { constructor(editor: HTMLTextAreaElement) {
this.editor = editor; this.editor = editor;
} }
insertPlaceholder(value) { insertPlaceholder(value: string) {
textareaInsertText(this.editor, value); textareaInsertText(this.editor, value);
} }
replacePlaceholder(oldVal, newVal) { replacePlaceholder(oldVal: string, newVal: string) {
const editor = this.editor; const editor = this.editor;
const startPos = editor.selectionStart; const startPos = editor.selectionStart;
const endPos = editor.selectionEnd; const endPos = editor.selectionEnd;
@ -65,11 +68,11 @@ class TextareaEditor {
class CodeMirrorEditor { class CodeMirrorEditor {
editor: CodeMirror.EditorFromTextArea; editor: CodeMirror.EditorFromTextArea;
constructor(editor) { constructor(editor: CodeMirror.EditorFromTextArea) {
this.editor = editor; this.editor = editor;
} }
insertPlaceholder(value) { insertPlaceholder(value: string) {
const editor = this.editor; const editor = this.editor;
const startPoint = editor.getCursor('start'); const startPoint = editor.getCursor('start');
const endPoint = editor.getCursor('end'); const endPoint = editor.getCursor('end');
@ -80,7 +83,7 @@ class CodeMirrorEditor {
triggerEditorContentChanged(editor.getTextArea()); triggerEditorContentChanged(editor.getTextArea());
} }
replacePlaceholder(oldVal, newVal) { replacePlaceholder(oldVal: string, newVal: string) {
const editor = this.editor; const editor = this.editor;
const endPoint = editor.getCursor('end'); const endPoint = editor.getCursor('end');
if (editor.getSelection() === oldVal) { 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(); e.preventDefault();
for (const file of files) { for (const file of files) {
const name = file.name.slice(0, file.name.lastIndexOf('.')); 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(`!?\\[([^\\]]+)\\]\\(/?attachments/${fileUuid}\\)`, 'g'), '');
text = text.replace(new RegExp(`<img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), ''); text = text.replace(new RegExp(`<img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), '');
return text; 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 // pasting with "shift" means "paste as original content" in most applications
if (isShiftDown) return; // let the browser handle it 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 // extract text and images from "paste" event
function getPastedContent(e) { function getPastedContent(e: ClipboardEvent) {
const images = []; const images = [];
for (const item of e.clipboardData?.items ?? []) { for (const item of e.clipboardData?.items ?? []) {
if (item.type?.startsWith('image/')) { if (item.type?.startsWith('image/')) {
@ -142,8 +145,8 @@ function getPastedContent(e) {
return {text, images}; return {text, images};
} }
export function initEasyMDEPaste(easyMDE, dropzoneEl) { export function initEasyMDEPaste(easyMDE: EasyMDE, dropzoneEl: HTMLElement) {
const editor = new CodeMirrorEditor(easyMDE.codemirror); const editor = new CodeMirrorEditor(easyMDE.codemirror as any);
easyMDE.codemirror.on('paste', (_, e) => { easyMDE.codemirror.on('paste', (_, e) => {
const {images} = getPastedContent(e); const {images} = getPastedContent(e);
if (!images.length) return; 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; let isShiftDown = false;
textarea.addEventListener('keydown', (e) => { textarea.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.shiftKey) isShiftDown = true; if (e.shiftKey) isShiftDown = true;
}); });
textarea.addEventListener('keyup', (e) => { textarea.addEventListener('keyup', (e: KeyboardEvent) => {
if (!e.shiftKey) isShiftDown = false; if (!e.shiftKey) isShiftDown = false;
}); });
textarea.addEventListener('paste', (e) => { textarea.addEventListener('paste', (e: ClipboardEvent) => {
const {images, text} = getPastedContent(e); const {images, text} = getPastedContent(e);
if (images.length && dropzoneEl) { if (images.length && dropzoneEl) {
handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, images, e); handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, images, e);
} else if (text) { } 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 (!e.dataTransfer.files.length) return;
if (!dropzoneEl) return; if (!dropzoneEl) return;
handleUploadFiles(new TextareaEditor(textarea), dropzoneEl, e.dataTransfer.files, e); 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); const newText = removeAttachmentLinksFromMarkdown(textarea.value, fileUuid);
if (textarea.value !== newText) textarea.value = newText; if (textarea.value !== newText) textarea.value = newText;
}); });

@ -1,6 +1,6 @@
import {querySingleVisibleElem} from '../../utils/dom.ts'; import {querySingleVisibleElem} from '../../utils/dom.ts';
export function handleGlobalEnterQuickSubmit(target) { export function handleGlobalEnterQuickSubmit(target: HTMLElement) {
let form = target.closest('form'); let form = target.closest('form');
if (form) { if (form) {
if (!form.checkValidity()) { if (!form.checkValidity()) {

@ -14,7 +14,7 @@ export function initCompSearchUserBox() {
minCharacters: 2, minCharacters: 2,
apiSettings: { apiSettings: {
url: `${appSubUrl}/user/search_candidates?q={query}`, url: `${appSubUrl}/user/search_candidates?q={query}`,
onResponse(response) { onResponse(response: any) {
const resultItems = []; const resultItems = [];
const searchQuery = searchUserBox.querySelector('input').value; const searchQuery = searchUserBox.querySelector('input').value;
const searchQueryUppercase = searchQuery.toUpperCase(); const searchQueryUppercase = searchQuery.toUpperCase();

@ -1,14 +1,20 @@
import {matchEmoji, matchMention, matchIssue} from '../../utils/match.ts'; import {matchEmoji, matchMention, matchIssue} from '../../utils/match.ts';
import {emojiString} from '../emoji.ts'; import {emojiString} from '../emoji.ts';
import {svg} from '../../svg.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 {createElementFromAttrs, createElementFromHTML} from '../../utils/dom.ts';
import {getIssueColor, getIssueIcon} from '../issue.ts'; import {getIssueColor, getIssueIcon} from '../issue.ts';
import {debounce} from 'perfect-debounce'; 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) => { const debouncedSuggestIssues = debounce((key: string, text: string) => new Promise<{matched:boolean; fragment?: HTMLElement}>(async (resolve) => {
let issuePathInfo = parseIssueHref(window.location.href); const issuePathInfo = parseIssueHref(window.location.href);
if (!issuePathInfo.ownerName) issuePathInfo = parseIssueNewHref(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}); if (!issuePathInfo.ownerName) return resolve({matched: false});
const matches = await matchIssue(issuePathInfo.ownerName, issuePathInfo.repoName, issuePathInfo.indexString, text); 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}); resolve({matched: true, fragment: ul});
}), 100); }), 100);
export function initTextExpander(expander) { export function initTextExpander(expander: TextExpanderElement) {
expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}) => { expander?.addEventListener('text-expander-change', ({detail: {key, provide, text}}: Record<string, any>) => {
if (key === ':') { if (key === ':') {
const matches = matchEmoji(text); const matches = matchEmoji(text);
if (!matches.length) return provide({matched: false}); if (!matches.length) return provide({matched: false});
@ -79,7 +85,7 @@ export function initTextExpander(expander) {
provide(debouncedSuggestIssues(key, text)); provide(debouncedSuggestIssues(key, text));
} }
}); });
expander?.addEventListener('text-expander-value', ({detail}) => { expander?.addEventListener('text-expander-value', ({detail}: Record<string, any>) => {
if (detail?.item) { if (detail?.item) {
// add a space after @mentions and #issue as it's likely the user wants one // add a space after @mentions and #issue as it's likely the user wants one
const suffix = ['@', '#'].includes(detail.key) ? ' ' : ''; const suffix = ['@', '#'].includes(detail.key) ? ' ' : '';

@ -4,11 +4,11 @@ import {parseIssueHref} from '../utils.ts';
import {createTippy} from '../modules/tippy.ts'; import {createTippy} from '../modules/tippy.ts';
export function initContextPopups() { export function initContextPopups() {
const refIssues = document.querySelectorAll('.ref-issue'); const refIssues = document.querySelectorAll<HTMLElement>('.ref-issue');
attachRefIssueContextPopup(refIssues); attachRefIssueContextPopup(refIssues);
} }
export function attachRefIssueContextPopup(refIssues) { export function attachRefIssueContextPopup(refIssues: NodeListOf<HTMLElement>) {
for (const refIssue of refIssues) { for (const refIssue of refIssues) {
if (refIssue.classList.contains('ref-external-issue')) continue; if (refIssue.classList.contains('ref-external-issue')) continue;

@ -46,7 +46,7 @@ export function initCopyContent() {
showTemporaryTooltip(btn, i18n.copy_success); showTemporaryTooltip(btn, i18n.copy_success);
} else { } else {
if (isRasterImage) { 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); showTemporaryTooltip(btn, success ? i18n.copy_success : i18n.copy_error);
} else { } else {
showTemporaryTooltip(btn, i18n.copy_error); showTemporaryTooltip(btn, i18n.copy_error);

@ -6,16 +6,18 @@ import {GET, POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts'; import {showErrorToast} from '../modules/toast.ts';
import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts'; import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts';
import {isImageFile, isVideoFile} from '../utils.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; const {csrfToken, i18n} = window.config;
type CustomDropzoneFile = DropzoneFile & {uuid: string};
// dropzone has its owner event dispatcher (emitter) // dropzone has its owner event dispatcher (emitter)
export const DropzoneCustomEventReloadFiles = 'dropzone-custom-reload-files'; export const DropzoneCustomEventReloadFiles = 'dropzone-custom-reload-files';
export const DropzoneCustomEventRemovedFile = 'dropzone-custom-removed-file'; export const DropzoneCustomEventRemovedFile = 'dropzone-custom-removed-file';
export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done'; 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([ const [{default: Dropzone}] = await Promise.all([
import(/* webpackChunkName: "dropzone" */'dropzone'), import(/* webpackChunkName: "dropzone" */'dropzone'),
import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'), import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'),
@ -23,7 +25,7 @@ async function createDropzone(el, opts) {
return new Dropzone(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})`; let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`;
if (isImageFile(file)) { if (isImageFile(file)) {
fileMarkdown = `!${fileMarkdown}`; fileMarkdown = `!${fileMarkdown}`;
@ -43,7 +45,7 @@ export function generateMarkdownLinkForAttachment(file, {width, dppx}: {width?:
return fileMarkdown; 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 // 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 // The "<a>" element has a hardcoded cursor: pointer because the default is overridden by .dropzone
const copyLinkEl = createElementFromHTML(` const copyLinkEl = createElementFromHTML(`
@ -58,6 +60,8 @@ function addCopyLink(file) {
file.previewTemplate.append(copyLinkEl); file.previewTemplate.append(copyLinkEl);
} }
type FileUuidDict = Record<string, {submitted: boolean}>;
/** /**
* @param {HTMLElement} dropzoneEl * @param {HTMLElement} dropzoneEl
*/ */
@ -67,7 +71,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) {
const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url'); const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url');
let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event 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> = { const opts: Record<string, any> = {
url: dropzoneEl.getAttribute('data-upload-url'), url: dropzoneEl.getAttribute('data-upload-url'),
headers: {'X-Csrf-Token': csrfToken}, headers: {'X-Csrf-Token': csrfToken},
@ -89,7 +93,7 @@ export async function initDropzone(dropzoneEl: HTMLElement) {
// "http://localhost:3000/owner/repo/issues/[object%20Event]" // "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]">' // 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); 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; file.uuid = resp.uuid;
fileUuidDict[file.uuid] = {submitted: false}; fileUuidDict[file.uuid] = {submitted: false};
const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${resp.uuid}`, value: resp.uuid}); 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.emit(DropzoneCustomEventUploadDone, {file});
}); });
dzInst.on('removedfile', async (file: DropzoneFile & {uuid: string}) => { dzInst.on('removedfile', async (file: CustomDropzoneFile) => {
if (disableRemovedfileEvent) return; if (disableRemovedfileEvent) return;
dzInst.emit(DropzoneCustomEventRemovedFile, {fileUuid: file.uuid}); dzInst.emit(DropzoneCustomEventRemovedFile, {fileUuid: file.uuid});

@ -15,13 +15,13 @@ export const emojiKeys = Object.keys(tempMap).sort((a, b) => {
return a.localeCompare(b); return a.localeCompare(b);
}); });
const emojiMap = {}; const emojiMap: Record<string, string> = {};
for (const key of emojiKeys) { for (const key of emojiKeys) {
emojiMap[key] = tempMap[key]; emojiMap[key] = tempMap[key];
} }
// retrieve HTML for given emoji name // retrieve HTML for given emoji name
export function emojiHTML(name) { export function emojiHTML(name: string) {
let inner; let inner;
if (Object.hasOwn(customEmojis, name)) { if (Object.hasOwn(customEmojis, name)) {
inner = `<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`; inner = `<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`;
@ -33,6 +33,6 @@ export function emojiHTML(name) {
} }
// retrieve string for given emoji name // retrieve string for given emoji name
export function emojiString(name) { export function emojiString(name: string) {
return emojiMap[name] || `:${name}:`; 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 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. // 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); 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) { if (newFold && fileContentBox.getBoundingClientRect().top < 0) {
fileContentBox.scrollIntoView(); fileContentBox.scrollIntoView();
} }
} }
// Like `setFileFolding`, except that it automatically inverts the current file folding state. // 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'); setFileFolding(fileContentBox, foldArrow, fileContentBox.getAttribute('data-folded') !== 'true');
} }

@ -7,7 +7,7 @@ export function initHeatmap() {
if (!el) return; if (!el) return;
try { try {
const heatmap = {}; const heatmap: Record<string, number> = {};
for (const {contributions, timestamp} of JSON.parse(el.getAttribute('data-heatmap-data'))) { for (const {contributions, timestamp} of JSON.parse(el.getAttribute('data-heatmap-data'))) {
// Convert to user timezone and sum contributions by date // Convert to user timezone and sum contributions by date
const dateStr = new Date(timestamp * 1000).toDateString(); 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 {parseDom} from '../utils.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts'; import {fomanticQuery} from '../modules/fomantic/base.ts';
function getDefaultSvgBoundsIfUndefined(text, src) { function getDefaultSvgBoundsIfUndefined(text: string, src: string) {
const defaultSize = 300; const defaultSize = 300;
const maxSize = 99999; const maxSize = 99999;
@ -38,7 +38,7 @@ function getDefaultSvgBoundsIfUndefined(text, src) {
return null; return null;
} }
function createContext(imageAfter, imageBefore) { function createContext(imageAfter: HTMLImageElement, imageBefore: HTMLImageElement) {
const sizeAfter = { const sizeAfter = {
width: imageAfter?.width || 0, width: imageAfter?.width || 0,
height: imageAfter?.height || 0, height: imageAfter?.height || 0,
@ -123,7 +123,7 @@ class ImageDiff {
queryElemChildren(containerEl, '.image-diff-tabs', (el) => el.classList.remove('is-loading')); queryElemChildren(containerEl, '.image-diff-tabs', (el) => el.classList.remove('is-loading'));
} }
initSideBySide(sizes) { initSideBySide(sizes: Record<string, any>) {
let factor = 1; let factor = 1;
if (sizes.maxSize.width > (this.diffContainerWidth - 24) / 2) { if (sizes.maxSize.width > (this.diffContainerWidth - 24) / 2) {
factor = (this.diffContainerWidth - 24) / 2 / sizes.maxSize.width; factor = (this.diffContainerWidth - 24) / 2 / sizes.maxSize.width;
@ -176,7 +176,7 @@ class ImageDiff {
} }
} }
initSwipe(sizes) { initSwipe(sizes: Record<string, any>) {
let factor = 1; let factor = 1;
if (sizes.maxSize.width > this.diffContainerWidth - 12) { if (sizes.maxSize.width > this.diffContainerWidth - 12) {
factor = (this.diffContainerWidth - 12) / sizes.maxSize.width; factor = (this.diffContainerWidth - 12) / sizes.maxSize.width;
@ -215,14 +215,14 @@ class ImageDiff {
this.containerEl.querySelector('.swipe-bar').addEventListener('mousedown', (e) => { this.containerEl.querySelector('.swipe-bar').addEventListener('mousedown', (e) => {
e.preventDefault(); e.preventDefault();
this.initSwipeEventListeners(e.currentTarget); this.initSwipeEventListeners(e.currentTarget as HTMLElement);
}); });
} }
initSwipeEventListeners(swipeBar) { initSwipeEventListeners(swipeBar: HTMLElement) {
const swipeFrame = swipeBar.parentNode; const swipeFrame = swipeBar.parentNode as HTMLElement;
const width = swipeFrame.clientWidth; const width = swipeFrame.clientWidth;
const onSwipeMouseMove = (e) => { const onSwipeMouseMove = (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
const rect = swipeFrame.getBoundingClientRect(); const rect = swipeFrame.getBoundingClientRect();
const value = Math.max(0, Math.min(e.clientX - rect.left, width)); const value = Math.max(0, Math.min(e.clientX - rect.left, width));
@ -237,7 +237,7 @@ class ImageDiff {
document.addEventListener('mouseup', removeEventListeners); document.addEventListener('mouseup', removeEventListeners);
} }
initOverlay(sizes) { initOverlay(sizes: Record<string, any>) {
let factor = 1; let factor = 1;
if (sizes.maxSize.width > this.diffContainerWidth - 12) { if (sizes.maxSize.width > this.diffContainerWidth - 12) {
factor = (this.diffContainerWidth - 12) / sizes.maxSize.width; factor = (this.diffContainerWidth - 12) / sizes.maxSize.width;

@ -12,11 +12,12 @@ export function initInstall() {
initPreInstall(); initPreInstall();
} }
} }
function initPreInstall() { function initPreInstall() {
const defaultDbUser = 'gitea'; const defaultDbUser = 'gitea';
const defaultDbName = 'gitea'; const defaultDbName = 'gitea';
const defaultDbHosts = { const defaultDbHosts: Record<string, string> = {
mysql: '127.0.0.1:3306', mysql: '127.0.0.1:3306',
postgres: '127.0.0.1:5432', postgres: '127.0.0.1:5432',
mssql: '127.0.0.1:1433', mssql: '127.0.0.1:1433',

@ -21,7 +21,7 @@ function initOrgTeamSearchRepoBox() {
minCharacters: 2, minCharacters: 2,
apiSettings: { apiSettings: {
url: `${appSubUrl}/repo/search?q={query}&uid=${$searchRepoBox.data('uid')}`, url: `${appSubUrl}/repo/search?q={query}&uid=${$searchRepoBox.data('uid')}`,
onResponse(response) { onResponse(response: any) {
const items = []; const items = [];
for (const item of response.data) { for (const item of response.data) {
items.push({ items.push({

@ -59,13 +59,13 @@ export function initViewedCheckboxListenerFor() {
const fileName = checkbox.getAttribute('name'); const fileName = checkbox.getAttribute('name');
// check if the file is in our difftreestore and if we find it -> change the IsViewed status // 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) { if (fileInPageData) {
fileInPageData.IsViewed = this.checked; fileInPageData.IsViewed = this.checked;
} }
// Unfortunately, actual forms cause too many problems, hence another approach is needed // Unfortunately, actual forms cause too many problems, hence another approach is needed
const files = {}; const files: Record<string, boolean> = {};
files[fileName] = this.checked; files[fileName] = this.checked;
const data: Record<string, any> = {files}; const data: Record<string, any> = {files};
const headCommitSHA = form.getAttribute('data-headcommit'); const headCommitSHA = form.getAttribute('data-headcommit');
@ -82,13 +82,13 @@ export function initViewedCheckboxListenerFor() {
export function initExpandAndCollapseFilesButton() { export function initExpandAndCollapseFilesButton() {
// expand btn // expand btn
document.querySelector(expandFilesBtnSelector)?.addEventListener('click', () => { 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); setFileFolding(box, box.querySelector('.fold-file'), false);
} }
}); });
// collapse btn, need to exclude the div of “show more” // collapse btn, need to exclude the div of “show more”
document.querySelector(collapseFilesBtnSelector)?.addEventListener('click', () => { 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; if (box.getAttribute('id') === 'diff-incomplete') continue;
setFileFolding(box, box.querySelector('.fold-file'), true); 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 {POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts'; import {showErrorToast} from '../modules/toast.ts';
import {sleep} from '../utils.ts'; import {sleep} from '../utils.ts';
@ -7,10 +7,10 @@ import {createApp} from 'vue';
import {toOriginUrl} from '../utils/url.ts'; import {toOriginUrl} from '../utils/url.ts';
import {createTippy} from '../modules/tippy.ts'; import {createTippy} from '../modules/tippy.ts';
async function onDownloadArchive(e) { async function onDownloadArchive(e: DOMEvent<MouseEvent>) {
e.preventDefault(); e.preventDefault();
// there are many places using the "archive-link", eg: the dropdown on the repo code page, the release list // 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; const targetLoading = el.closest('.ui.dropdown') ?? el;
targetLoading.classList.add('is-loading', 'loading-icon-2px'); targetLoading.classList.add('is-loading', 'loading-icon-2px');
try { try {
@ -107,7 +107,7 @@ export function initRepoCloneButtons() {
queryElems(document, '.clone-buttons-combo', initCloneSchemeUrlSelection); 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 { try {
const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})}); const response = await POST(url, {data: new URLSearchParams({action, issue_ids, id})});
if (!response.ok) { if (!response.ok) {

@ -168,7 +168,7 @@ function onShowMoreFiles() {
initDiffHeaderPopup(); initDiffHeaderPopup();
} }
export async function loadMoreFiles(url) { export async function loadMoreFiles(url: string) {
const target = document.querySelector('a#diff-show-more-files'); const target = document.querySelector('a#diff-show-more-files');
if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) { if (target?.classList.contains('disabled') || pageData.diffFileInfo.isLoadingNewData) {
return; return;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save