diff --git a/docs/content/doc/features/comparison.en-us.md b/docs/content/doc/features/comparison.en-us.md index 745c5d37bc729..a703766855f4b 100644 --- a/docs/content/doc/features/comparison.en-us.md +++ b/docs/content/doc/features/comparison.en-us.md @@ -50,7 +50,7 @@ _Symbols used in table:_ | Repository Tokens with write rights | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | | Built-in Container Registry | [✘](https://github.com/go-gitea/gitea/issues/2316) | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | | External git mirroring | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | -| WebAuthn (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ? | +| FIDO U2F (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | | Built-in CI/CD | ✘ | ✘ | ✓ | ✓ | ✓ | ✘ | ✘ | | Subgroups: groups within groups | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ | @@ -66,7 +66,6 @@ _Symbols used in table:_ | Granular user roles (Code, Issues, Wiki etc) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | | Verified Committer | ⁄ | ✘ | ? | ✓ | ✓ | ✓ | ✘ | | GPG Signed Commits | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| SSH Signed Commits | ✓ | ✘ | ✘ | ✘ | ✘ | ? | ? | | Reject unsigned commits | [✓](https://github.com/go-gitea/gitea/pull/9708) | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | | Repository Activity page | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | | Branch manager | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | diff --git a/docs/content/doc/features/comparison.zh-cn.md b/docs/content/doc/features/comparison.zh-cn.md index 98a50f5dc2abd..8ccdebe8cd94d 100644 --- a/docs/content/doc/features/comparison.zh-cn.md +++ b/docs/content/doc/features/comparison.zh-cn.md @@ -48,7 +48,7 @@ _表格中的符号含义:_ | 仓库写权限令牌 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✓ | | 内置容器 Registry | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | | 外部 Git 镜像 | ✓ | ✓ | ✘ | ✘ | ✓ | ✓ | ✓ | -| WebAuthn (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ? | +| FIDO U2F (2FA) | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✘ | | 内置 CI/CD | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | | 子组织:组织内的组织 | ✘ | ✘ | ✘ | ✓ | ✓ | ✘ | ✓ | @@ -64,7 +64,6 @@ _表格中的符号含义:_ | 细粒度用户角色 (例如 Code, Issues, Wiki) | ✓ | ✘ | ✘ | ✓ | ✓ | ✘ | ✘ | | 提交人的身份验证 | ✘ | ✘ | ? | ✓ | ✓ | ✓ | ✘ | | GPG 签名的提交 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | -| SSH 签名的提交 | ✓ | ✘ | ✘ | ✘ | ✘ | ? | ? | | 拒绝未用通过验证的提交 | ✓ | ✘ | ✓ | ✓ | ✓ | ✘ | ✓ | | 仓库活跃度页面 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | | 分支管理 | ✓ | ✘ | ✓ | ✓ | ✓ | ✓ | ✓ | diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go index 1d19ebd24e76b..8a9a3ffd1d6aa 100644 --- a/models/webhook/hooktask.go +++ b/models/webhook/hooktask.go @@ -116,6 +116,9 @@ type HookTask struct { RequestInfo *HookRequest `xorm:"-"` ResponseContent string `xorm:"TEXT"` ResponseInfo *HookResponse `xorm:"-"` + + // Used for Auth Headers. + BearerToken string } func init() { diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go index ffc9b72b64d88..4e6d0ebf68f82 100644 --- a/models/webhook/webhook.go +++ b/models/webhook/webhook.go @@ -158,6 +158,7 @@ const ( DINGTALK HookType = "dingtalk" TELEGRAM HookType = "telegram" MSTEAMS HookType = "msteams" + TEAMCITY HookType = "teamcity" FEISHU HookType = "feishu" MATRIX HookType = "matrix" WECHATWORK HookType = "wechatwork" diff --git a/modules/queue/workerpool.go b/modules/queue/workerpool.go index 100197c5e1f1f..4b36ca6f76bfa 100644 --- a/modules/queue/workerpool.go +++ b/modules/queue/workerpool.go @@ -115,9 +115,6 @@ func (p *WorkerPool) hasNoWorkerScaling() bool { return p.numberOfWorkers == 0 && (p.boostTimeout == 0 || p.boostWorkers == 0 || p.maxNumberOfWorkers == 0) } -// zeroBoost will add a temporary boost worker for a no worker queue -// p.lock must be locked at the start of this function BUT it will be unlocked by the end of this function -// (This is because addWorkers has to be called whilst unlocked) func (p *WorkerPool) zeroBoost() { ctx, cancel := context.WithTimeout(p.baseCtx, p.boostTimeout) mq := GetManager().GetManagedQueue(p.qid) diff --git a/modules/setting/webhook.go b/modules/setting/webhook.go index 0bfd7dcb4dd3f..755a9d88422cc 100644 --- a/modules/setting/webhook.go +++ b/modules/setting/webhook.go @@ -36,7 +36,7 @@ func newWebhookService() { Webhook.DeliverTimeout = sec.Key("DELIVER_TIMEOUT").MustInt(5) Webhook.SkipTLSVerify = sec.Key("SKIP_TLS_VERIFY").MustBool() Webhook.AllowedHostList = sec.Key("ALLOWED_HOST_LIST").MustString("") - Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "matrix", "wechatwork", "packagist"} + Webhook.Types = []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "teamcity", "feishu", "matrix", "wechatwork", "packagist"} Webhook.PagingNum = sec.Key("PAGING_NUM").MustInt(10) Webhook.ProxyURL = sec.Key("PROXY_URL").MustString("") if Webhook.ProxyURL != "" { diff --git a/modules/structs/hook.go b/modules/structs/hook.go index e4d7652c72a99..76f6943f2e029 100644 --- a/modules/structs/hook.go +++ b/modules/structs/hook.go @@ -40,7 +40,7 @@ type CreateHookOptionConfig map[string]string // CreateHookOption options when create a hook type CreateHookOption struct { // required: true - // enum: dingtalk,discord,gitea,gogs,msteams,slack,telegram,feishu,wechatwork,packagist + // enum: dingtalk,discord,gitea,gogs,msteams,teamcity,slack,telegram,feishu,wechatwork,packagist Type string `json:"type" binding:"Required"` // required: true Config CreateHookOptionConfig `json:"config" binding:"Required"` diff --git a/options/locale/locale_bg-BG.ini b/options/locale/locale_bg-BG.ini index 4354ed1cdeb12..1cd0edecb175f 100644 --- a/options/locale/locale_bg-BG.ini +++ b/options/locale/locale_bg-BG.ini @@ -891,6 +891,9 @@ settings.hook_type=Тип на куката settings.slack_token=API ключ settings.slack_domain=Домейн settings.slack_channel=Канал +settings.teamcity_host_url = Host URL +settings.teamcity_vcs_root_id = VCS Root ID +settings.teamcity_auth_token = Authentication Token settings.deploy_keys=Ключове за внедряване settings.add_deploy_key=Добави ключ за внедряване settings.title=Заглавие diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e91016bdc0506..7ce0f6737c8ea 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1943,6 +1943,9 @@ settings.hook_type = Hook Type settings.slack_token = Token settings.slack_domain = Domain settings.slack_channel = Channel +settings.teamcity_host_url = Host URL +settings.teamcity_vcs_root_id = VCS Root ID +settings.teamcity_auth_token = Authentication Token settings.add_web_hook_desc = Integrate %s into your repository. settings.web_hook_name_gitea = Gitea settings.web_hook_name_gogs = Gogs @@ -1952,6 +1955,7 @@ settings.web_hook_name_dingtalk = DingTalk settings.web_hook_name_telegram = Telegram settings.web_hook_name_matrix = Matrix settings.web_hook_name_msteams = Microsoft Teams +settings.web_hook_name_teamcity = TeamCity settings.web_hook_name_feishu_or_larksuite = Feishu / Lark Suite settings.web_hook_name_feishu = Feishu settings.web_hook_name_larksuite = Lark Suite diff --git a/public/img/teamcity.svg b/public/img/teamcity.svg new file mode 100644 index 0000000000000..ca14b3dc1d44f --- /dev/null +++ b/public/img/teamcity.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/routers/web/repo/webhook.go b/routers/web/repo/webhook.go index fb984de7f5857..9b93c446a483b 100644 --- a/routers/web/repo/webhook.go +++ b/routers/web/repo/webhook.go @@ -535,6 +535,66 @@ func MSTeamsHooksNewPost(ctx *context.Context) { ctx.Redirect(orCtx.Link) } +// TeamCityHooksNewPost response for creating TeamCity hook +func TeamCityHooksNewPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.NewTeamCityHookForm) + ctx.Data["Title"] = ctx.Tr("repo.settings.add_webhook") + ctx.Data["PageIsSettingHooks"] = true + ctx.Data["PageIsSettingHooksNew"] = true + ctx.Data["Webhook"] = webhook.Webhook{HookEvent: &webhook.HookEvent{}} + ctx.Data["HookType"] = webhook.TEAMCITY + + orCtx, err := getOrgRepoCtx(ctx) + if err != nil { + ctx.ServerError("getOrgRepoCtx", err) + } + ctx.Data["BaseLink"] = orCtx.Link + + if ctx.HasError() { + ctx.HTML(200, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&webhook_service.TeamCityMeta{ + HostURL: form.HostURL, + AuthToken: form.AuthToken, + VcsRootID: form.VcsRootID, + }) + if err != nil { + ctx.ServerError("Marshal", err) + return + } + + payloadURL, err := buildTeamCityURL(form) + if err != nil { + ctx.ServerError("buildTeamCityURL", err) + return + } + + w := &webhook.Webhook{ + RepoID: orCtx.RepoID, + URL: payloadURL, + ContentType: webhook.ContentTypeForm, + HookEvent: ParseHookEvent(form.WebhookForm), + IsActive: form.Active, + Type: webhook.TEAMCITY, + HTTPMethod: http.MethodPost, + Meta: string(meta), + OrgID: orCtx.OrgID, + IsSystemWebhook: orCtx.IsSystemWebhook, + } + if err := w.UpdateEvent(); err != nil { + ctx.ServerError("UpdateEvent", err) + return + } else if err := webhook.CreateWebhook(db.DefaultContext, w); err != nil { + ctx.ServerError("CreateWebhook", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.add_hook_success")) + ctx.Redirect(orCtx.Link) +} + // SlackHooksNewPost response for creating slack hook func SlackHooksNewPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.NewSlackHookForm) @@ -770,6 +830,8 @@ func checkWebhook(ctx *context.Context) (*orgRepoCtx, *webhook.Webhook) { ctx.Data["DiscordHook"] = webhook_service.GetDiscordHook(w) case webhook.TELEGRAM: ctx.Data["TelegramHook"] = webhook_service.GetTelegramHook(w) + case webhook.TEAMCITY: + ctx.Data["TeamCityHook"] = webhook_service.GetTeamCityHook(w) case webhook.MATRIX: ctx.Data["MatrixHook"] = webhook_service.GetMatrixHook(w) case webhook.PACKAGIST: @@ -1126,6 +1188,70 @@ func MSTeamsHooksEditPost(ctx *context.Context) { ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID)) } +// TeamCityHooksEditPost response for editing teamcity hook +func TeamCityHooksEditPost(ctx *context.Context) { + form := web.GetForm(ctx).(*forms.NewTeamCityHookForm) + + ctx.Data["Title"] = ctx.Tr("repo.settings") + ctx.Data["PageIsSettingHooks"] = true + ctx.Data["PageIsSettingHooksEdit"] = true + + orCtx, w := checkWebhook(ctx) + + if ctx.Written() { + return + } + + ctx.Data["Webhook"] = w + + if ctx.HasError() { + ctx.HTML(200, orCtx.NewTemplate) + return + } + + meta, err := json.Marshal(&webhook_service.TeamCityMeta{ + HostURL: form.HostURL, + AuthToken: form.AuthToken, + VcsRootID: form.VcsRootID, + }) + if err != nil { + ctx.ServerError("Marshal", err) + return + } + + w.URL, err = buildTeamCityURL(form) + if err != nil { + ctx.ServerError("buildTeamCityURL", err) + return + } + + w.HTTPMethod = http.MethodPost + w.Meta = string(meta) + w.HookEvent = ParseHookEvent(form.WebhookForm) + w.IsActive = form.Active + + if err := w.UpdateEvent(); err != nil { + ctx.ServerError("UpdateEvent", err) + return + } else if err := webhook.UpdateWebhook(w); err != nil { + ctx.ServerError("UpdateWebhook", err) + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.update_hook_success")) + ctx.Redirect(fmt.Sprintf("%s/%d", orCtx.Link, w.ID)) +} + +// buildTeamCityURL returns the correct REST API url for a TeamCity POST request. +func buildTeamCityURL(meta *forms.NewTeamCityHookForm) (string, error) { + tcURL, err := url.Parse(meta.HostURL) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s/app/rest/vcs-root-instances/commitHookNotification?locator=vcsRoot:%s", tcURL, meta.VcsRootID), nil +} + // FeishuHooksEditPost response for editing feishu hook func FeishuHooksEditPost(ctx *context.Context) { form := web.GetForm(ctx).(*forms.NewFeishuHookForm) diff --git a/routers/web/web.go b/routers/web/web.go index 60a379aef8580..dd532f029e524 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -447,6 +447,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) + m.Post("/teamcity/{id}", bindIgnErr(forms.NewTeamCityHookForm{}), repo.TeamCityHooksEditPost) m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) m.Post("/packagist/{id}", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) @@ -462,6 +463,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) + m.Post("/teamcity/new", bindIgnErr(forms.NewTeamCityHookForm{}), repo.TeamCityHooksNewPost) m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) m.Post("/packagist/new", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) @@ -561,6 +563,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) + m.Post("/teamcity/new", bindIgnErr(forms.NewTeamCityHookForm{}), repo.TeamCityHooksNewPost) m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) m.Group("/{id}", func() { @@ -575,6 +578,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) + m.Post("/teamcity/{id}", bindIgnErr(forms.NewTeamCityHookForm{}), repo.TeamCityHooksEditPost) m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) }, webhooksEnabled) @@ -658,6 +662,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/telegram/new", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksNewPost) m.Post("/matrix/new", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksNewPost) m.Post("/msteams/new", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksNewPost) + m.Post("/teamcity/new", bindIgnErr(forms.NewTeamCityHookForm{}), repo.TeamCityHooksNewPost) m.Post("/feishu/new", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksNewPost) m.Post("/wechatwork/new", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksNewPost) m.Post("/packagist/new", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksNewPost) @@ -674,6 +679,7 @@ func RegisterRoutes(m *web.Route) { m.Post("/telegram/{id}", bindIgnErr(forms.NewTelegramHookForm{}), repo.TelegramHooksEditPost) m.Post("/matrix/{id}", bindIgnErr(forms.NewMatrixHookForm{}), repo.MatrixHooksEditPost) m.Post("/msteams/{id}", bindIgnErr(forms.NewMSTeamsHookForm{}), repo.MSTeamsHooksEditPost) + m.Post("/teamcity/{id}", bindIgnErr(forms.NewTeamCityHookForm{}), repo.TeamCityHooksEditPost) m.Post("/feishu/{id}", bindIgnErr(forms.NewFeishuHookForm{}), repo.FeishuHooksEditPost) m.Post("/wechatwork/{id}", bindIgnErr(forms.NewWechatWorkHookForm{}), repo.WechatworkHooksEditPost) m.Post("/packagist/{id}", bindIgnErr(forms.NewPackagistHookForm{}), repo.PackagistHooksEditPost) diff --git a/services/forms/repo_form.go b/services/forms/repo_form.go index b32bd3cafd9d3..2963aafdc5fcc 100644 --- a/services/forms/repo_form.go +++ b/services/forms/repo_form.go @@ -372,6 +372,19 @@ func (f *NewMSTeamsHookForm) Validate(req *http.Request, errs binding.Errors) bi return middleware.Validate(errs, ctx.Data, f, ctx.Locale) } +type NewTeamCityHookForm struct { + HostURL string `binding:"Required;ValidUrl"` + AuthToken string `binding:"Required"` + VcsRootID string `binding:"Required"` + WebhookForm +} + +// Validate validates the fields +func (f *NewTeamCityHookForm) Validate(req *http.Request, errs binding.Errors) binding.Errors { + ctx := context.GetContext(req) + return middleware.Validate(errs, ctx.Data, f, ctx.Locale) +} + // NewFeishuHookForm form for creating feishu hook type NewFeishuHookForm struct { PayloadURL string `binding:"Required;ValidUrl"` diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go index 5639a08f96401..6f285ec467c63 100644 --- a/services/mirror/mirror.go +++ b/services/mirror/mirror.go @@ -59,13 +59,11 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error { handler := func(idx int, bean interface{}, limit int) error { var item SyncRequest - var repo *repo_model.Repository if m, ok := bean.(*repo_model.Mirror); ok { if m.Repo == nil { log.Error("Disconnected mirror found: %d", m.ID) return nil } - repo = m.Repo item = SyncRequest{ Type: PullMirrorType, RepoID: m.RepoID, @@ -75,7 +73,6 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error { log.Error("Disconnected push-mirror found: %d", m.ID) return nil } - repo = m.Repo item = SyncRequest{ Type: PushMirrorType, RepoID: m.RepoID, @@ -92,16 +89,17 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error { default: } + // Check if this request is already in the queue + has, err := mirrorQueue.Has(&item) + if err != nil { + return err + } + if has { + return nil + } + // Push to the Queue if err := mirrorQueue.Push(&item); err != nil { - if err == queue.ErrAlreadyInQueue { - if item.Type == PushMirrorType { - log.Trace("PushMirrors for %-v already queued for sync", repo) - } else { - log.Trace("PullMirrors for %-v already queued for sync", repo) - } - return nil - } return err } @@ -112,29 +110,23 @@ func Update(ctx context.Context, pullLimit, pushLimit int) error { return nil } - pullMirrorsRequested := 0 if pullLimit != 0 { - requested = 0 if err := repo_model.MirrorsIterate(func(idx int, bean interface{}) error { return handler(idx, bean, pullLimit) }); err != nil && err != errLimit { log.Error("MirrorsIterate: %v", err) return err } - pullMirrorsRequested, requested = requested, 0 } - pushMirrorsRequested := 0 if pushLimit != 0 { - requested = 0 if err := repo_model.PushMirrorsIterate(func(idx int, bean interface{}) error { return handler(idx, bean, pushLimit) }); err != nil && err != errLimit { log.Error("PushMirrorsIterate: %v", err) return err } - pushMirrorsRequested, requested = requested, 0 } - log.Trace("Finished: Update: %d pull mirrors and %d push mirrors queued", pullMirrorsRequested, pushMirrorsRequested) + log.Trace("Finished: Update") return nil } diff --git a/services/webhook/deliver.go b/services/webhook/deliver.go index 88b709cb41e74..45b263f288105 100644 --- a/services/webhook/deliver.go +++ b/services/webhook/deliver.go @@ -116,6 +116,11 @@ func Deliver(t *webhook_model.HookTask) error { event := t.EventType.Event() eventType := string(t.EventType) + + if t.BearerToken != "" { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", t.BearerToken)) + } + req.Header.Add("X-Gitea-Delivery", t.UUID) req.Header.Add("X-Gitea-Event", event) req.Header.Add("X-Gitea-Event-Type", eventType) diff --git a/services/webhook/teamcity.go b/services/webhook/teamcity.go new file mode 100644 index 0000000000000..79238831bdb76 --- /dev/null +++ b/services/webhook/teamcity.go @@ -0,0 +1,36 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file.package webhook + +package webhook + +import ( + webhook_model "code.gitea.io/gitea/models/webhook" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" +) + +type ( + // TeamCityMeta contains metadata for the TeamCity WebHook + TeamCityMeta struct { + HostURL string `json:"host_url"` + AuthToken string `json:"auth_token"` + VcsRootID string `json:"vcs_root_id"` + } +) + +// GetTeamCityPayload returns the payload as-is +func GetTeamCityPayload(p api.Payloader, event webhook_model.HookEventType, meta string) (api.Payloader, error) { + // TeamCity requests API doesn't take a body on POST, so no need to alter it in any way. + return p, nil +} + +// GetTeamCityHook returns TeamCity metadata +func GetTeamCityHook(w *webhook_model.Webhook) *TeamCityMeta { + s := &TeamCityMeta{} + if err := json.Unmarshal([]byte(w.Meta), s); err != nil { + log.Error("webhook.GetTeamCityHook(%d): %v", w.ID, err) + } + return s +} diff --git a/services/webhook/teamcity_test.go b/services/webhook/teamcity_test.go new file mode 100644 index 0000000000000..987c92b778920 --- /dev/null +++ b/services/webhook/teamcity_test.go @@ -0,0 +1,39 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file.package webhook + +package webhook + +import ( + "testing" + + webhook_model "code.gitea.io/gitea/models/webhook" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetTeamCityPayload(t *testing.T) { + t.Run("Payload isn't altered.", func(t *testing.T) { + p := createTestPayload() + + pl, err := GetTeamCityPayload(p, webhook_model.HookEventPush, "") + require.NoError(t, err) + require.Equal(t, p, pl) + }) +} + +func TestWebhook_GetTeamCityHook(t *testing.T) { + t.Run("GetTeamCityHook", func(t *testing.T) { + w := &webhook_model.Webhook{ + Meta: `{"host_url": "http://localhost.com", "auth_token": "testToken", "vcs_root_id" :"fooVCS"}`, + } + + teamcityHook := GetTeamCityHook(w) + assert.Equal(t, *teamcityHook, TeamCityMeta{ + HostURL: "http://localhost.com", + AuthToken: "testToken", + VcsRootID: "fooVCS", + }) + }) +} diff --git a/services/webhook/webhook.go b/services/webhook/webhook.go index 607fac963452f..7497ac3ba0026 100644 --- a/services/webhook/webhook.go +++ b/services/webhook/webhook.go @@ -46,6 +46,10 @@ var webhooks = map[webhook_model.HookType]*webhook{ name: webhook_model.MSTEAMS, payloadCreator: GetMSTeamsPayload, }, + webhook_model.TEAMCITY: { + name: webhook_model.TEAMCITY, + payloadCreator: GetTeamCityPayload, + }, webhook_model.FEISHU: { name: webhook_model.FEISHU, payloadCreator: GetFeishuPayload, @@ -170,11 +174,19 @@ func prepareWebhook(w *webhook_model.Webhook, repo *repo_model.Repository, event payloader = p } + // Load any auth/bearer tokens... + var authToken string + switch w.Type { + case webhook_model.TEAMCITY: + authToken = GetTeamCityHook(w).AuthToken + } + if err = webhook_model.CreateHookTask(&webhook_model.HookTask{ - RepoID: repo.ID, - HookID: w.ID, - Payloader: payloader, - EventType: event, + RepoID: repo.ID, + HookID: w.ID, + BearerToken: authToken, + Payloader: payloader, + EventType: event, }); err != nil { return fmt.Errorf("CreateHookTask: %v", err) } diff --git a/templates/admin/hook_new.tmpl b/templates/admin/hook_new.tmpl index 049e54ef833cf..506d3380b9373 100644 --- a/templates/admin/hook_new.tmpl +++ b/templates/admin/hook_new.tmpl @@ -28,6 +28,8 @@ {{else if eq .HookType "msteams"}} + {{else if eq .HookType "teamcity"}} + {{else if eq .HookType "feishu"}} {{else if eq .HookType "matrix"}} @@ -47,6 +49,7 @@ {{template "repo/settings/webhook/dingtalk" .}} {{template "repo/settings/webhook/telegram" .}} {{template "repo/settings/webhook/msteams" .}} + {{template "repo/settings/webhook/teamcity" .}} {{template "repo/settings/webhook/feishu" .}} {{template "repo/settings/webhook/matrix" .}} {{template "repo/settings/webhook/wechatwork" .}} diff --git a/templates/org/settings/hook_new.tmpl b/templates/org/settings/hook_new.tmpl index 5e8ebb51e9427..0aad537d4a447 100644 --- a/templates/org/settings/hook_new.tmpl +++ b/templates/org/settings/hook_new.tmpl @@ -23,6 +23,8 @@ {{else if eq .HookType "msteams"}} + {{else if eq .HookType "teamcity"}} + {{else if eq .HookType "feishu"}} {{else if eq .HookType "matrix"}} @@ -42,6 +44,7 @@ {{template "repo/settings/webhook/dingtalk" .}} {{template "repo/settings/webhook/telegram" .}} {{template "repo/settings/webhook/msteams" .}} + {{template "repo/settings/webhook/teamcity" .}} {{template "repo/settings/webhook/feishu" .}} {{template "repo/settings/webhook/matrix" .}} {{template "repo/settings/webhook/wechatwork" .}} diff --git a/templates/repo/settings/webhook/base_list.tmpl b/templates/repo/settings/webhook/base_list.tmpl index b1a3771bdba11..5e065cbe52761 100644 --- a/templates/repo/settings/webhook/base_list.tmpl +++ b/templates/repo/settings/webhook/base_list.tmpl @@ -25,6 +25,9 @@ {{.i18n.Tr "repo.settings.web_hook_name_msteams"}} + + {{.i18n.Tr "repo.settings.web_hook_name_teamcity"}} + {{.i18n.Tr "repo.settings.web_hook_name_feishu_or_larksuite"}} diff --git a/templates/repo/settings/webhook/new.tmpl b/templates/repo/settings/webhook/new.tmpl index a438a4c71a3d7..3d8b4602a77a6 100644 --- a/templates/repo/settings/webhook/new.tmpl +++ b/templates/repo/settings/webhook/new.tmpl @@ -21,6 +21,8 @@ {{else if eq .HookType "msteams"}} + {{else if eq .HookType "teamcity"}} + {{else if eq .HookType "feishu"}} {{else if eq .HookType "matrix"}} @@ -40,6 +42,7 @@ {{template "repo/settings/webhook/dingtalk" .}} {{template "repo/settings/webhook/telegram" .}} {{template "repo/settings/webhook/msteams" .}} + {{template "repo/settings/webhook/teamcity" .}} {{template "repo/settings/webhook/feishu" .}} {{template "repo/settings/webhook/matrix" .}} {{template "repo/settings/webhook/wechatwork" .}} diff --git a/templates/repo/settings/webhook/teamcity.tmpl b/templates/repo/settings/webhook/teamcity.tmpl new file mode 100644 index 0000000000000..3ccf6245b1a9d --- /dev/null +++ b/templates/repo/settings/webhook/teamcity.tmpl @@ -0,0 +1,19 @@ +{{if eq .HookType "teamcity"}} +

{{.i18n.Tr "repo.settings.add_web_hook_desc" "https://jetbrains.com/teamcity" (.i18n.Tr "repo.settings.web_hook_name_teamcity") | Str2html}}

+
+ {{.CsrfTokenHtml}} +
+ + +
+
+ + +
+
+ + +
+ {{template "repo/settings/webhook/settings" .}} +
+{{end}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 497636e78140a..0bce0752e2bae 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -13514,6 +13514,7 @@ "gitea", "gogs", "msteams", + "teamcity", "slack", "telegram", "feishu",