mirror of
https://github.com/go-gitea/gitea.git
synced 2025-02-02 15:09:33 -05:00
Merge branch 'main' into feat-version-arch
This commit is contained in:
commit
39efb64c1d
@ -18,10 +18,12 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/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)
|
|
||||||
}
|
}
|
||||||
|
if w.PushOnly {
|
||||||
// HasDeleteEvent returns true if hook enabled delete event.
|
return evt == webhook_module.HookEventPush
|
||||||
func (w *Webhook) HasDeleteEvent() bool {
|
|
||||||
return w.SendEverything ||
|
|
||||||
(w.ChooseEvents && w.HookEvents.Delete)
|
|
||||||
}
|
}
|
||||||
|
checkEvt := evt
|
||||||
// HasForkEvent returns true if hook enabled fork event.
|
switch evt {
|
||||||
func (w *Webhook) HasForkEvent() bool {
|
case webhook_module.HookEventPullRequestReviewApproved, webhook_module.HookEventPullRequestReviewRejected, webhook_module.HookEventPullRequestReviewComment:
|
||||||
return w.SendEverything ||
|
checkEvt = webhook_module.HookEventPullRequestReview
|
||||||
(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},
|
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
for _, c := range w.EventCheckers() {
|
if w.PushOnly {
|
||||||
if c.Has() {
|
return []string{string(webhook_module.HookEventPush)}
|
||||||
events = append(events, string(c.Type))
|
}
|
||||||
|
|
||||||
|
events := make([]string, 0, len(w.HookEvents))
|
||||||
|
for event, enabled := range w.HookEvents {
|
||||||
|
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())
|
||||||
}
|
}
|
||||||
|
32
modules/gtprof/event.go
Normal file
32
modules/gtprof/event.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package gtprof
|
||||||
|
|
||||||
|
type EventConfig struct {
|
||||||
|
attributes []*TraceAttribute
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventOption interface {
|
||||||
|
applyEvent(*EventConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
type applyEventFunc func(*EventConfig)
|
||||||
|
|
||||||
|
func (f applyEventFunc) applyEvent(cfg *EventConfig) {
|
||||||
|
f(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithAttributes(attrs ...*TraceAttribute) EventOption {
|
||||||
|
return applyEventFunc(func(cfg *EventConfig) {
|
||||||
|
cfg.attributes = append(cfg.attributes, attrs...)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func eventConfigFromOptions(options ...EventOption) *EventConfig {
|
||||||
|
cfg := &EventConfig{}
|
||||||
|
for _, opt := range options {
|
||||||
|
opt.applyEvent(cfg)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
175
modules/gtprof/trace.go
Normal file
175
modules/gtprof/trace.go
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package gtprof
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type contextKey struct {
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
var contextKeySpan = &contextKey{"span"}
|
||||||
|
|
||||||
|
type traceStarter interface {
|
||||||
|
start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal)
|
||||||
|
}
|
||||||
|
|
||||||
|
type traceSpanInternal interface {
|
||||||
|
addEvent(name string, cfg *EventConfig)
|
||||||
|
recordError(err error, cfg *EventConfig)
|
||||||
|
end()
|
||||||
|
}
|
||||||
|
|
||||||
|
type TraceSpan struct {
|
||||||
|
// immutable
|
||||||
|
parent *TraceSpan
|
||||||
|
internalSpans []traceSpanInternal
|
||||||
|
internalContexts []context.Context
|
||||||
|
|
||||||
|
// mutable, must be protected by mutex
|
||||||
|
mu sync.RWMutex
|
||||||
|
name string
|
||||||
|
statusCode uint32
|
||||||
|
statusDesc string
|
||||||
|
startTime time.Time
|
||||||
|
endTime time.Time
|
||||||
|
attributes []*TraceAttribute
|
||||||
|
children []*TraceSpan
|
||||||
|
}
|
||||||
|
|
||||||
|
type TraceAttribute struct {
|
||||||
|
Key string
|
||||||
|
Value TraceValue
|
||||||
|
}
|
||||||
|
|
||||||
|
type TraceValue struct {
|
||||||
|
v any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TraceValue) AsString() string {
|
||||||
|
return fmt.Sprint(t.v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TraceValue) AsInt64() int64 {
|
||||||
|
v, _ := util.ToInt64(t.v)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TraceValue) AsFloat64() float64 {
|
||||||
|
v, _ := util.ToFloat64(t.v)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalTraceStarters []traceStarter
|
||||||
|
|
||||||
|
type Tracer struct {
|
||||||
|
starters []traceStarter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TraceSpan) SetName(name string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TraceSpan) SetStatus(code uint32, desc string) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.statusCode, s.statusDesc = code, desc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TraceSpan) AddEvent(name string, options ...EventOption) {
|
||||||
|
cfg := eventConfigFromOptions(options...)
|
||||||
|
for _, tsp := range s.internalSpans {
|
||||||
|
tsp.addEvent(name, cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TraceSpan) RecordError(err error, options ...EventOption) {
|
||||||
|
cfg := eventConfigFromOptions(options...)
|
||||||
|
for _, tsp := range s.internalSpans {
|
||||||
|
tsp.recordError(err, cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TraceSpan) SetAttributeString(key, value string) *TraceSpan {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.attributes = append(s.attributes, &TraceAttribute{Key: key, Value: TraceValue{v: value}})
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tracer) Start(ctx context.Context, spanName string) (context.Context, *TraceSpan) {
|
||||||
|
starters := t.starters
|
||||||
|
if starters == nil {
|
||||||
|
starters = globalTraceStarters
|
||||||
|
}
|
||||||
|
ts := &TraceSpan{name: spanName, startTime: time.Now()}
|
||||||
|
parentSpan := GetContextSpan(ctx)
|
||||||
|
if parentSpan != nil {
|
||||||
|
parentSpan.mu.Lock()
|
||||||
|
parentSpan.children = append(parentSpan.children, ts)
|
||||||
|
parentSpan.mu.Unlock()
|
||||||
|
ts.parent = parentSpan
|
||||||
|
}
|
||||||
|
|
||||||
|
parentCtx := ctx
|
||||||
|
for internalSpanIdx, tsp := range starters {
|
||||||
|
var internalSpan traceSpanInternal
|
||||||
|
if parentSpan != nil {
|
||||||
|
parentCtx = parentSpan.internalContexts[internalSpanIdx]
|
||||||
|
}
|
||||||
|
ctx, internalSpan = tsp.start(parentCtx, ts, internalSpanIdx)
|
||||||
|
ts.internalContexts = append(ts.internalContexts, ctx)
|
||||||
|
ts.internalSpans = append(ts.internalSpans, internalSpan)
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, contextKeySpan, ts)
|
||||||
|
return ctx, ts
|
||||||
|
}
|
||||||
|
|
||||||
|
type mutableContext interface {
|
||||||
|
context.Context
|
||||||
|
SetContextValue(key, value any)
|
||||||
|
GetContextValue(key any) any
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartInContext starts a trace span in Gitea's mutable context (usually the web request context).
|
||||||
|
// Due to the design limitation of Gitea's web framework, it can't use `context.WithValue` to bind a new span into a new context.
|
||||||
|
// So here we use our "reqctx" framework to achieve the same result: web request context could always see the latest "span".
|
||||||
|
func (t *Tracer) StartInContext(ctx mutableContext, spanName string) (*TraceSpan, func()) {
|
||||||
|
curTraceSpan := GetContextSpan(ctx)
|
||||||
|
_, newTraceSpan := GetTracer().Start(ctx, spanName)
|
||||||
|
ctx.SetContextValue(contextKeySpan, newTraceSpan)
|
||||||
|
return newTraceSpan, func() {
|
||||||
|
newTraceSpan.End()
|
||||||
|
ctx.SetContextValue(contextKeySpan, curTraceSpan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TraceSpan) End() {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.endTime = time.Now()
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
for _, tsp := range s.internalSpans {
|
||||||
|
tsp.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTracer() *Tracer {
|
||||||
|
return &Tracer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetContextSpan(ctx context.Context) *TraceSpan {
|
||||||
|
ts, _ := ctx.Value(contextKeySpan).(*TraceSpan)
|
||||||
|
return ts
|
||||||
|
}
|
96
modules/gtprof/trace_builtin.go
Normal file
96
modules/gtprof/trace_builtin.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package gtprof
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/tailmsg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type traceBuiltinStarter struct{}
|
||||||
|
|
||||||
|
type traceBuiltinSpan struct {
|
||||||
|
ts *TraceSpan
|
||||||
|
|
||||||
|
internalSpanIdx int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *traceBuiltinSpan) addEvent(name string, cfg *EventConfig) {
|
||||||
|
// No-op because builtin tracer doesn't need it.
|
||||||
|
// In the future we might use it to mark the time point between backend logic and network response.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *traceBuiltinSpan) recordError(err error, cfg *EventConfig) {
|
||||||
|
// No-op because builtin tracer doesn't need it.
|
||||||
|
// Actually Gitea doesn't handle err this way in most cases
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *traceBuiltinSpan) toString(out *strings.Builder, indent int) {
|
||||||
|
t.ts.mu.RLock()
|
||||||
|
defer t.ts.mu.RUnlock()
|
||||||
|
|
||||||
|
out.WriteString(strings.Repeat(" ", indent))
|
||||||
|
out.WriteString(t.ts.name)
|
||||||
|
if t.ts.endTime.IsZero() {
|
||||||
|
out.WriteString(" duration: (not ended)")
|
||||||
|
} else {
|
||||||
|
out.WriteString(fmt.Sprintf(" duration=%.4fs", t.ts.endTime.Sub(t.ts.startTime).Seconds()))
|
||||||
|
}
|
||||||
|
for _, a := range t.ts.attributes {
|
||||||
|
out.WriteString(" ")
|
||||||
|
out.WriteString(a.Key)
|
||||||
|
out.WriteString("=")
|
||||||
|
value := a.Value.AsString()
|
||||||
|
if strings.ContainsAny(value, " \t\r\n") {
|
||||||
|
quoted := false
|
||||||
|
for _, c := range "\"'`" {
|
||||||
|
if quoted = !strings.Contains(value, string(c)); quoted {
|
||||||
|
value = string(c) + value + string(c)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !quoted {
|
||||||
|
value = fmt.Sprintf("%q", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.WriteString(value)
|
||||||
|
}
|
||||||
|
out.WriteString("\n")
|
||||||
|
for _, c := range t.ts.children {
|
||||||
|
span := c.internalSpans[t.internalSpanIdx].(*traceBuiltinSpan)
|
||||||
|
span.toString(out, indent+2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *traceBuiltinSpan) end() {
|
||||||
|
if t.ts.parent == nil {
|
||||||
|
// TODO: debug purpose only
|
||||||
|
// TODO: it should distinguish between http response network lag and actual processing time
|
||||||
|
threshold := time.Duration(traceBuiltinThreshold.Load())
|
||||||
|
if threshold != 0 && t.ts.endTime.Sub(t.ts.startTime) > threshold {
|
||||||
|
sb := &strings.Builder{}
|
||||||
|
t.toString(sb, 0)
|
||||||
|
tailmsg.GetManager().GetTraceRecorder().Record(sb.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *traceBuiltinStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) {
|
||||||
|
return ctx, &traceBuiltinSpan{ts: traceSpan, internalSpanIdx: internalSpanIdx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
globalTraceStarters = append(globalTraceStarters, &traceBuiltinStarter{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var traceBuiltinThreshold atomic.Int64
|
||||||
|
|
||||||
|
func EnableBuiltinTracer(threshold time.Duration) {
|
||||||
|
traceBuiltinThreshold.Store(int64(threshold))
|
||||||
|
}
|
19
modules/gtprof/trace_const.go
Normal file
19
modules/gtprof/trace_const.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package gtprof
|
||||||
|
|
||||||
|
// Some interesting names could be found in https://github.com/open-telemetry/opentelemetry-go/tree/main/semconv
|
||||||
|
|
||||||
|
const (
|
||||||
|
TraceSpanHTTP = "http"
|
||||||
|
TraceSpanGitRun = "git-run"
|
||||||
|
TraceSpanDatabase = "database"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TraceAttrFuncCaller = "func.caller"
|
||||||
|
TraceAttrDbSQL = "db.sql"
|
||||||
|
TraceAttrGitCommand = "git.command"
|
||||||
|
TraceAttrHTTPRoute = "http.route"
|
||||||
|
)
|
93
modules/gtprof/trace_test.go
Normal file
93
modules/gtprof/trace_test.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package gtprof
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// "vendor span" is a simple demo for a span from a vendor library
|
||||||
|
|
||||||
|
var vendorContextKey any = "vendorContextKey"
|
||||||
|
|
||||||
|
type vendorSpan struct {
|
||||||
|
name string
|
||||||
|
children []*vendorSpan
|
||||||
|
}
|
||||||
|
|
||||||
|
func vendorTraceStart(ctx context.Context, name string) (context.Context, *vendorSpan) {
|
||||||
|
span := &vendorSpan{name: name}
|
||||||
|
parentSpan, ok := ctx.Value(vendorContextKey).(*vendorSpan)
|
||||||
|
if ok {
|
||||||
|
parentSpan.children = append(parentSpan.children, span)
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, vendorContextKey, span)
|
||||||
|
return ctx, span
|
||||||
|
}
|
||||||
|
|
||||||
|
// below "testTrace*" integrate the vendor span into our trace system
|
||||||
|
|
||||||
|
type testTraceSpan struct {
|
||||||
|
vendorSpan *vendorSpan
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testTraceSpan) addEvent(name string, cfg *EventConfig) {}
|
||||||
|
|
||||||
|
func (t *testTraceSpan) recordError(err error, cfg *EventConfig) {}
|
||||||
|
|
||||||
|
func (t *testTraceSpan) end() {}
|
||||||
|
|
||||||
|
type testTraceStarter struct{}
|
||||||
|
|
||||||
|
func (t *testTraceStarter) start(ctx context.Context, traceSpan *TraceSpan, internalSpanIdx int) (context.Context, traceSpanInternal) {
|
||||||
|
ctx, span := vendorTraceStart(ctx, traceSpan.name)
|
||||||
|
return ctx, &testTraceSpan{span}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTraceStarter(t *testing.T) {
|
||||||
|
globalTraceStarters = []traceStarter{&testTraceStarter{}}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx, span := GetTracer().Start(ctx, "root")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
func(ctx context.Context) {
|
||||||
|
ctx, span := GetTracer().Start(ctx, "span1")
|
||||||
|
defer span.End()
|
||||||
|
func(ctx context.Context) {
|
||||||
|
_, span := GetTracer().Start(ctx, "spanA")
|
||||||
|
defer span.End()
|
||||||
|
}(ctx)
|
||||||
|
func(ctx context.Context) {
|
||||||
|
_, span := GetTracer().Start(ctx, "spanB")
|
||||||
|
defer span.End()
|
||||||
|
}(ctx)
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
func(ctx context.Context) {
|
||||||
|
_, span := GetTracer().Start(ctx, "span2")
|
||||||
|
defer span.End()
|
||||||
|
}(ctx)
|
||||||
|
|
||||||
|
var spanFullNames []string
|
||||||
|
var collectSpanNames func(parentFullName string, s *vendorSpan)
|
||||||
|
collectSpanNames = func(parentFullName string, s *vendorSpan) {
|
||||||
|
fullName := parentFullName + "/" + s.name
|
||||||
|
spanFullNames = append(spanFullNames, fullName)
|
||||||
|
for _, c := range s.children {
|
||||||
|
collectSpanNames(fullName, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collectSpanNames("", span.internalSpans[0].(*testTraceSpan).vendorSpan)
|
||||||
|
assert.Equal(t, []string{
|
||||||
|
"/root",
|
||||||
|
"/root/span1",
|
||||||
|
"/root/span1/spanA",
|
||||||
|
"/root/span1/spanB",
|
||||||
|
"/root/span2",
|
||||||
|
}, spanFullNames)
|
||||||
|
}
|
@ -46,6 +46,7 @@ var Service = struct {
|
|||||||
RequireSignInView bool
|
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()
|
||||||
|
73
modules/tailmsg/talimsg.go
Normal file
73
modules/tailmsg/talimsg.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package tailmsg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MsgRecord struct {
|
||||||
|
Time time.Time
|
||||||
|
Content string
|
||||||
|
}
|
||||||
|
|
||||||
|
type MsgRecorder interface {
|
||||||
|
Record(content string)
|
||||||
|
GetRecords() []*MsgRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
type memoryMsgRecorder struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
msgs []*MsgRecord
|
||||||
|
limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use redis for a clustered environment
|
||||||
|
|
||||||
|
func (m *memoryMsgRecorder) Record(content string) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
m.msgs = append(m.msgs, &MsgRecord{
|
||||||
|
Time: time.Now(),
|
||||||
|
Content: content,
|
||||||
|
})
|
||||||
|
if len(m.msgs) > m.limit {
|
||||||
|
m.msgs = m.msgs[len(m.msgs)-m.limit:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memoryMsgRecorder) GetRecords() []*MsgRecord {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
ret := make([]*MsgRecord, len(m.msgs))
|
||||||
|
copy(ret, m.msgs)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMsgRecorder(limit int) MsgRecorder {
|
||||||
|
return &memoryMsgRecorder{
|
||||||
|
limit: limit,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
traceRecorder MsgRecorder
|
||||||
|
logRecorder MsgRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetTraceRecorder() MsgRecorder {
|
||||||
|
return m.traceRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) GetLogRecorder() MsgRecorder {
|
||||||
|
return m.logRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
var GetManager = sync.OnceValue(func() *Manager {
|
||||||
|
return &Manager{
|
||||||
|
traceRecorder: NewMsgRecorder(100),
|
||||||
|
logRecorder: NewMsgRecorder(1000),
|
||||||
|
}
|
||||||
|
})
|
@ -6,6 +6,9 @@ package routing
|
|||||||
import (
|
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
|
||||||
|
20
modules/webhook/events.go
Normal file
20
modules/webhook/events.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package webhook
|
||||||
|
|
||||||
|
type HookEvents map[HookEventType]bool
|
||||||
|
|
||||||
|
func (he HookEvents) Get(evt HookEventType) bool {
|
||||||
|
return he[evt]
|
||||||
|
}
|
||||||
|
|
||||||
|
// HookEvent represents events that will delivery hook.
|
||||||
|
type HookEvent struct {
|
||||||
|
PushOnly bool `json:"push_only"`
|
||||||
|
SendEverything bool `json:"send_everything"`
|
||||||
|
ChooseEvents bool `json:"choose_events"`
|
||||||
|
BranchFilter string `json:"branch_filter"`
|
||||||
|
|
||||||
|
HookEvents `json:"events"`
|
||||||
|
}
|
@ -1,39 +0,0 @@
|
|||||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
package webhook
|
|
||||||
|
|
||||||
// HookEvents is a set of web hook events
|
|
||||||
type HookEvents struct {
|
|
||||||
Create bool `json:"create"`
|
|
||||||
Delete bool `json:"delete"`
|
|
||||||
Fork bool `json:"fork"`
|
|
||||||
Issues bool `json:"issues"`
|
|
||||||
IssueAssign bool `json:"issue_assign"`
|
|
||||||
IssueLabel bool `json:"issue_label"`
|
|
||||||
IssueMilestone bool `json:"issue_milestone"`
|
|
||||||
IssueComment bool `json:"issue_comment"`
|
|
||||||
Push bool `json:"push"`
|
|
||||||
PullRequest bool `json:"pull_request"`
|
|
||||||
PullRequestAssign bool `json:"pull_request_assign"`
|
|
||||||
PullRequestLabel bool `json:"pull_request_label"`
|
|
||||||
PullRequestMilestone bool `json:"pull_request_milestone"`
|
|
||||||
PullRequestComment bool `json:"pull_request_comment"`
|
|
||||||
PullRequestReview bool `json:"pull_request_review"`
|
|
||||||
PullRequestSync bool `json:"pull_request_sync"`
|
|
||||||
PullRequestReviewRequest bool `json:"pull_request_review_request"`
|
|
||||||
Wiki bool `json:"wiki"`
|
|
||||||
Repository bool `json:"repository"`
|
|
||||||
Release bool `json:"release"`
|
|
||||||
Package bool `json:"package"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HookEvent represents events that will delivery hook.
|
|
||||||
type HookEvent struct {
|
|
||||||
PushOnly bool `json:"push_only"`
|
|
||||||
SendEverything bool `json:"send_everything"`
|
|
||||||
ChooseEvents bool `json:"choose_events"`
|
|
||||||
BranchFilter string `json:"branch_filter"`
|
|
||||||
|
|
||||||
HookEvents `json:"events"`
|
|
||||||
}
|
|
@ -31,21 +31,47 @@ const (
|
|||||||
HookEventRepository HookEventType = "repository"
|
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
988
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@ -32,7 +32,7 @@
|
|||||||
"htmx.org": "2.0.4",
|
"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"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
18
routers/web/admin/perftrace.go
Normal file
18
routers/web/admin/perftrace.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/tailmsg"
|
||||||
|
"code.gitea.io/gitea/services/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PerfTrace(ctx *context.Context) {
|
||||||
|
monitorTraceCommon(ctx)
|
||||||
|
ctx.Data["PageIsAdminMonitorPerfTrace"] = true
|
||||||
|
ctx.Data["PerfTraceRecords"] = tailmsg.GetManager().GetTraceRecorder().GetRecords()
|
||||||
|
ctx.HTML(http.StatusOK, tplPerfTrace)
|
||||||
|
}
|
@ -12,10 +12,17 @@ import (
|
|||||||
"code.gitea.io/gitea/services/context"
|
"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,16 +137,10 @@ 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 {
|
|
||||||
if !e.Has() {
|
|
||||||
return nil
|
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.).
|
||||||
// Integration webhooks (e.g. drone) still receive the required data.
|
// Integration webhooks (e.g. drone) still receive the required data.
|
||||||
if pushEvent, ok := p.(*api.PushPayload); ok &&
|
if pushEvent, ok := p.(*api.PushPayload); ok &&
|
||||||
|
@ -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>
|
||||||
|
13
templates/admin/perftrace.tmpl
Normal file
13
templates/admin/perftrace.tmpl
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{{template "admin/layout_head" (dict "ctxData" . "pageClass" "admin monitor")}}
|
||||||
|
|
||||||
|
<div class="admin-setting-content">
|
||||||
|
{{template "admin/trace_tabs" .}}
|
||||||
|
|
||||||
|
{{range $record := .PerfTraceRecords}}
|
||||||
|
<div class="ui segment tw-w-full tw-overflow-auto">
|
||||||
|
<pre class="tw-whitespace-pre">{{$record.Content}}</pre>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{template "admin/layout_footer" .}}
|
@ -17,7 +17,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<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" .}}
|
||||||
|
19
templates/admin/trace_tabs.tmpl
Normal file
19
templates/admin/trace_tabs.tmpl
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<div class="flex-text-block">
|
||||||
|
<div class="tw-flex-1">
|
||||||
|
<div class="ui compact small menu">
|
||||||
|
{{if .ShowAdminPerformanceTraceTab}}
|
||||||
|
<a class="item {{Iif .PageIsAdminMonitorPerfTrace "active"}}" href="{{AppSubUrl}}/-/admin/monitor/perftrace">{{ctx.Locale.Tr "admin.monitor.performance_logs"}}</a>
|
||||||
|
{{end}}
|
||||||
|
<a class="item {{Iif (eq .ShowGoroutineList "process") "active"}}" href="{{AppSubUrl}}/-/admin/monitor/stacktrace?show=process">{{ctx.Locale.Tr "admin.monitor.process"}}</a>
|
||||||
|
<a class="item {{Iif (eq .ShowGoroutineList "stacktrace") "active"}}" href="{{AppSubUrl}}/-/admin/monitor/stacktrace?show=stacktrace">{{ctx.Locale.Tr "admin.monitor.stacktrace"}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form target="_blank" action="{{AppSubUrl}}/-/admin/monitor/diagnosis" class="ui form">
|
||||||
|
<div class="ui inline field">
|
||||||
|
<button class="ui primary small button">{{ctx.Locale.Tr "admin.monitor.download_diagnosis_report"}}</button>
|
||||||
|
<input name="seconds" size="3" maxlength="3" value="10"> {{ctx.Locale.Tr "tool.raw_seconds"}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
@ -1,5 +1,6 @@
|
|||||||
<button class="ui primary button js-btn-clone-panel">
|
<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">
|
||||||
|
{{if .EnablePasskeyAuth}}
|
||||||
|
{{template "user/auth/webauthn_error" .}}
|
||||||
<a class="signin-passkey">{{ctx.Locale.Tr "auth.signin_passkey"}}</a>
|
<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,11 +49,20 @@ 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;
|
||||||
@ -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…
Reference in New Issue
Block a user