From 7cf56a16d29622b5103fe2954d7535450b8e2b41 Mon Sep 17 00:00:00 2001 From: cnphpbb Date: Sat, 2 Nov 2019 01:53:08 +0800 Subject: [PATCH 01/14] =?UTF-8?q?doc:=E5=A2=9E=E5=8A=A0=E9=99=84=E4=BB=B6`?= =?UTF-8?q?ALLOWED=5FTYPES`=E5=8F=96=E5=BE=97MIME=20type=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E6=B3=95=20(#8770)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../doc/advanced/config-cheat-sheet.zh-cn.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md index a0e33c637022..53426ed9834d 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +++ b/docs/content/doc/advanced/config-cheat-sheet.zh-cn.md @@ -173,6 +173,20 @@ menu: - `MAX_SIZE`: 附件最大限制,单位 MB,比如: `4`。 - `MAX_FILES`: 一次最多上传的附件数量,比如: `5`。 +关于 `ALLOWED_TYPES`, 在 (*)unix 系统中可以使用`file -I ` 来快速获得对应的 `MIME type`。 + +```shell +$ file -I test00.tar.xz +test00.tar.xz: application/x-xz; charset=binary + +$ file --mime test00.xlsx +test00.xlsx: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=binary + +file -I test01.xls +test01.xls: application/vnd.ms-excel; charset=binary +``` + + ## Log (`log`) - `ROOT_PATH`: 日志文件根目录。 From 9175556da0e89ec82bd3311990e34dd83dc7e3df Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Fri, 1 Nov 2019 18:20:30 +0000 Subject: [PATCH 02/14] [skip ci] Updated translations via Crowdin --- options/locale/locale_tr-TR.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/options/locale/locale_tr-TR.ini b/options/locale/locale_tr-TR.ini index 1dccf7a2a0bf..ac93db57c02a 100644 --- a/options/locale/locale_tr-TR.ini +++ b/options/locale/locale_tr-TR.ini @@ -1865,6 +1865,7 @@ delete_branch=%[3]s deposundan %[2]s dalı silindi compare_commits=%d işlemeyi karşılaştır compare_commits_general=İşlemeleri karşılaştır mirror_sync_push=işlemeler yansıdan %[4]s deposundaki %[3]s dalına eşitlendi +mirror_sync_create=%[2]s referansı %[3]s için yansıdan senkronize edildi mirror_sync_delete=%[3]s adresindeki %[2]s referansı eşitlendi ve silindi [tool] From ba336f6f456835f1f327ee967991079dd220266d Mon Sep 17 00:00:00 2001 From: Mario Lubenka Date: Fri, 1 Nov 2019 23:02:41 +0100 Subject: [PATCH 03/14] Unifies commit list in repository commit table and wiki revision page (#7907) Signed-off-by: Mario Lubenka --- modules/templates/helper.go | 12 +++++ templates/repo/commits_list.tmpl | 87 +++++++++++++++++++++++++++++++ templates/repo/commits_table.tmpl | 68 +----------------------- templates/repo/wiki/revision.tmpl | 55 +------------------ 4 files changed, 101 insertions(+), 121 deletions(-) create mode 100644 templates/repo/commits_list.tmpl diff --git a/modules/templates/helper.go b/modules/templates/helper.go index bdcaa12754ab..2d7a1aee9b4f 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -246,6 +246,18 @@ func NewFuncMap() []template.FuncMap { "MirrorFullAddress": mirror_service.AddressNoCredentials, "MirrorUserName": mirror_service.Username, "MirrorPassword": mirror_service.Password, + "CommitType": func(commit interface{}) string { + switch commit.(type) { + case models.SignCommitWithStatuses: + return "SignCommitWithStatuses" + case models.SignCommit: + return "SignCommit" + case models.UserCommit: + return "UserCommit" + default: + return "" + } + }, }} } diff --git a/templates/repo/commits_list.tmpl b/templates/repo/commits_list.tmpl new file mode 100644 index 000000000000..177c434724ff --- /dev/null +++ b/templates/repo/commits_list.tmpl @@ -0,0 +1,87 @@ +
+ + + + + + + + + + + {{ $r:= List .Commits}} + {{range $r}} + + + + + + + {{end}} + +
{{.i18n.Tr "repo.commits.author"}}SHA1{{.i18n.Tr "repo.commits.message"}}{{.i18n.Tr "repo.commits.date"}}
+ {{$userName := .Author.Name}} + {{if .User}} + {{if .User.FullName}} + {{$userName = .User.FullName}} + {{end}} +   {{$userName}} + {{else}} +   {{$userName}} + {{end}} + + {{$class := "ui sha label"}} + {{if .Signature}} + {{$class = (printf "%s%s" $class " isSigned")}} + {{if .Verification.Verified}} + {{$class = (printf "%s%s" $class " isVerified")}} + {{else if .Verification.Warning}} + {{$class = (printf "%s%s" $class " isWarning")}} + {{end}} + {{end}} + {{if $.Reponame}} + + {{else}} + + {{end}} + {{ShortSha .ID.String}} + {{if .Signature}} +
+ {{if .Verification.Verified}} + + {{if ne .Verification.SigningUser.ID 0}} + + {{else}} + + + + + {{end}} + {{else if .Verification.Warning}} + + {{else}} + + {{end}} +
+ {{end}} + {{if $.Reponame}} +
+ {{else}} + + {{end}} +
+ + {{ $commitLink:= printf "%s/%s/%s/commit/%s" AppSubUrl $.Username $.Reponame .ID }} + {{RenderCommitMessageLinkSubject .Message $.RepoLink $commitLink $.Repository.ComposeMetas}} + + {{if IsMultilineCommitMessage .Message}} + + {{end}} + {{if eq (CommitType .) "SignCommitWithStatuses"}} + {{template "repo/commit_status" .Status}} + {{end}} + {{if IsMultilineCommitMessage .Message}} + + {{end}} + {{TimeSince .Author.When $.Lang}}
+
diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl index 09a2c072b139..93c321cbe7b7 100644 --- a/templates/repo/commits_table.tmpl +++ b/templates/repo/commits_table.tmpl @@ -30,73 +30,7 @@ {{if and .Commits (gt .CommitCount 0)}} -
- - - - - - - - - - - {{ $r:= List .Commits}} - {{range $r}} - - - - - - - {{end}} - -
{{.i18n.Tr "repo.commits.author"}}SHA1{{.i18n.Tr "repo.commits.message"}}{{.i18n.Tr "repo.commits.date"}}
- {{if .User}} - {{if .User.FullName}} -   {{.User.FullName}} - {{else}} -   {{.Author.Name}} - {{end}} - {{else}} -   {{.Author.Name}} - {{end}} - - - {{ShortSha .ID.String}} - {{if .Signature}} -
- {{if .Verification.Verified}} - {{if ne .Verification.SigningUser.ID 0}} - - {{else}} - - - - - {{end}} - {{else if .Verification.Warning}} - - {{else}} - - {{end}} -
- {{end}} -
-
- - {{ $commitLink:= printf "%s/%s/%s/commit/%s" AppSubUrl $.Username $.Reponame .ID }} - {{RenderCommitMessageLinkSubject .Message $.RepoLink $commitLink $.Repository.ComposeMetas}} - - {{if IsMultilineCommitMessage .Message}} - - {{end}} - {{template "repo/commit_status" .Status}} - {{if IsMultilineCommitMessage .Message}} - - {{end}} - {{TimeSince .Author.When $.Lang}}
-
+ {{template "repo/commits_list" .}} {{end}} {{template "base/paginate" .}} diff --git a/templates/repo/wiki/revision.tmpl b/templates/repo/wiki/revision.tmpl index c3a4f7636b35..13d3a6227f31 100644 --- a/templates/repo/wiki/revision.tmpl +++ b/templates/repo/wiki/revision.tmpl @@ -48,60 +48,7 @@ {{if and .Commits (gt .CommitCount 0)}} -
- - - - - - - - - - - {{ $r:= List .Commits}} - {{range $r}} - - - - - - - {{end}} - -
{{.i18n.Tr "repo.commits.author"}}SHA1{{.i18n.Tr "repo.commits.message"}}{{.i18n.Tr "repo.commits.date"}}
- {{if .User}} - {{if .User.FullName}} -   {{.User.FullName}} - {{else}} -   {{.Author.Name}} - {{end}} - {{else}} -   {{.Author.Name}} - {{end}} - - - - - {{.Summary}} - {{if IsMultilineCommitMessage .Message}} - - - {{end}} - - {{TimeSince .Author.When $.Lang}}
-
+ {{template "repo/commits_list" .}} {{end}} {{template "base/paginate" .}} From 0e7f7df3cf176640c66ddf286ec052c7c13beb8a Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 2 Nov 2019 06:51:22 +0800 Subject: [PATCH 04/14] Move webhook to a standalone package under modules (#8747) * Move webhook to a standalone package under modules * fix test * fix comments --- go.mod | 2 + models/webhook.go | 379 ++---------------------- models/webhook_test.go | 54 ---- modules/notification/webhook/webhook.go | 63 ++-- modules/repofiles/action.go | 13 +- modules/webhook/deliver.go | 208 +++++++++++++ modules/webhook/main_test.go | 16 + modules/webhook/webhook.go | 179 +++++++++++ modules/webhook/webhook_test.go | 67 +++++ routers/api/v1/repo/hook.go | 5 +- routers/init.go | 3 +- routers/repo/pull.go | 3 +- routers/repo/webhook.go | 5 +- services/issue/label.go | 7 +- services/milestone/milestone.go | 7 +- services/mirror/sync.go | 5 +- services/pull/merge.go | 5 +- services/pull/pull.go | 9 +- services/pull/review.go | 5 +- services/release/release.go | 13 +- 20 files changed, 570 insertions(+), 478 deletions(-) create mode 100644 modules/webhook/deliver.go create mode 100644 modules/webhook/main_test.go create mode 100644 modules/webhook/webhook.go create mode 100644 modules/webhook/webhook_test.go diff --git a/go.mod b/go.mod index 02f0c46f22e1..7e920ac03208 100644 --- a/go.mod +++ b/go.mod @@ -69,6 +69,8 @@ require ( github.com/mattn/go-sqlite3 v1.11.0 github.com/mcuadros/go-version v0.0.0-20190308113854-92cdf37c5b75 github.com/microcosm-cc/bluemonday v0.0.0-20161012083705-f77f16ffc87a + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae // indirect github.com/msteinert/pam v0.0.0-20151204160544-02ccfbfaf0cc github.com/nfnt/resize v0.0.0-20160724205520-891127d8d1b5 diff --git a/models/webhook.go b/models/webhook.go index 6f2162c799d2..d3a8b52d86a0 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -6,34 +6,18 @@ package models import ( - "crypto/hmac" - "crypto/sha256" - "crypto/tls" - "encoding/hex" "encoding/json" "fmt" - "io/ioutil" - "net" - "net/http" - "net/url" - "strings" "time" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/sync" "code.gitea.io/gitea/modules/timeutil" - "github.com/gobwas/glob" gouuid "github.com/satori/go.uuid" - "github.com/unknwon/com" ) -// HookQueue is a global queue of web hooks -var HookQueue = sync.NewUniqueQueue(setting.Webhook.QueueLength) - // HookContentType is the content type of a web hook type HookContentType int @@ -227,13 +211,14 @@ func (w *Webhook) HasRepositoryEvent() bool { (w.ChooseEvents && w.HookEvents.Repository) } -func (w *Webhook) eventCheckers() []struct { - has func() bool - typ HookEventType +// EventCheckers returns event checkers +func (w *Webhook) EventCheckers() []struct { + Has func() bool + Type HookEventType } { return []struct { - has func() bool - typ HookEventType + Has func() bool + Type HookEventType }{ {w.HasCreateEvent, HookEventCreate}, {w.HasDeleteEvent, HookEventDelete}, @@ -251,29 +236,14 @@ func (w *Webhook) eventCheckers() []struct { func (w *Webhook) EventsArray() []string { events := make([]string, 0, 7) - for _, c := range w.eventCheckers() { - if c.has() { - events = append(events, string(c.typ)) + for _, c := range w.EventCheckers() { + if c.Has() { + events = append(events, string(c.Type)) } } return events } -func (w *Webhook) checkBranch(branch string) bool { - if w.BranchFilter == "" || w.BranchFilter == "*" { - return true - } - - g, err := glob.Compile(w.BranchFilter) - if err != nil { - // should not really happen as BranchFilter is validated - log.Error("CheckBranch failed: %s", err) - return false - } - - return g.Match(branch) -} - // CreateWebhook creates a new web hook. func CreateWebhook(w *Webhook) error { return createWebhook(x, w) @@ -664,329 +634,20 @@ func UpdateHookTask(t *HookTask) error { return err } -// PrepareWebhook adds special webhook to task queue for given payload. -func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Payloader) error { - return prepareWebhook(x, w, repo, event, p) -} - -// getPayloadBranch returns branch for hook event, if applicable. -func getPayloadBranch(p api.Payloader) string { - switch pp := p.(type) { - case *api.CreatePayload: - if pp.RefType == "branch" { - return pp.Ref - } - case *api.DeletePayload: - if pp.RefType == "branch" { - return pp.Ref - } - case *api.PushPayload: - if strings.HasPrefix(pp.Ref, git.BranchPrefix) { - return pp.Ref[len(git.BranchPrefix):] - } - } - return "" -} - -func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType, p api.Payloader) error { - for _, e := range w.eventCheckers() { - if event == e.typ { - if !e.has() { - return nil - } - } - } - - // If payload has no associated branch (e.g. it's a new tag, issue, etc.), - // branch filter has no effect. - if branch := getPayloadBranch(p); branch != "" { - if !w.checkBranch(branch) { - log.Info("Branch %q doesn't match branch filter %q, skipping", branch, w.BranchFilter) - return nil - } - } - - var payloader api.Payloader - var err error - // Use separate objects so modifications won't be made on payload on non-Gogs/Gitea type hooks. - switch w.HookTaskType { - case SLACK: - payloader, err = GetSlackPayload(p, event, w.Meta) - if err != nil { - return fmt.Errorf("GetSlackPayload: %v", err) - } - case DISCORD: - payloader, err = GetDiscordPayload(p, event, w.Meta) - if err != nil { - return fmt.Errorf("GetDiscordPayload: %v", err) - } - case DINGTALK: - payloader, err = GetDingtalkPayload(p, event, w.Meta) - if err != nil { - return fmt.Errorf("GetDingtalkPayload: %v", err) - } - case TELEGRAM: - payloader, err = GetTelegramPayload(p, event, w.Meta) - if err != nil { - return fmt.Errorf("GetTelegramPayload: %v", err) - } - case MSTEAMS: - payloader, err = GetMSTeamsPayload(p, event, w.Meta) - if err != nil { - return fmt.Errorf("GetMSTeamsPayload: %v", err) - } - default: - p.SetSecret(w.Secret) - payloader = p - } - - var signature string - if len(w.Secret) > 0 { - data, err := payloader.JSONPayload() - if err != nil { - log.Error("prepareWebhooks.JSONPayload: %v", err) - } - sig := hmac.New(sha256.New, []byte(w.Secret)) - _, err = sig.Write(data) - if err != nil { - log.Error("prepareWebhooks.sigWrite: %v", err) - } - signature = hex.EncodeToString(sig.Sum(nil)) - } - - if err = createHookTask(e, &HookTask{ - RepoID: repo.ID, - HookID: w.ID, - Type: w.HookTaskType, - URL: w.URL, - Signature: signature, - Payloader: payloader, - HTTPMethod: w.HTTPMethod, - ContentType: w.ContentType, - EventType: event, - IsSSL: w.IsSSL, - }); err != nil { - return fmt.Errorf("CreateHookTask: %v", err) - } - return nil -} - -// PrepareWebhooks adds new webhooks to task queue for given payload. -func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) error { - return prepareWebhooks(x, repo, event, p) -} - -func prepareWebhooks(e Engine, repo *Repository, event HookEventType, p api.Payloader) error { - ws, err := getActiveWebhooksByRepoID(e, repo.ID) - if err != nil { - return fmt.Errorf("GetActiveWebhooksByRepoID: %v", err) - } - - // check if repo belongs to org and append additional webhooks - if repo.mustOwner(e).IsOrganization() { - // get hooks for org - orgHooks, err := getActiveWebhooksByOrgID(e, repo.OwnerID) - if err != nil { - return fmt.Errorf("GetActiveWebhooksByOrgID: %v", err) - } - ws = append(ws, orgHooks...) - } - - if len(ws) == 0 { - return nil - } - - for _, w := range ws { - if err = prepareWebhook(e, w, repo, event, p); err != nil { - return err - } - } - return nil -} - -func (t *HookTask) deliver() error { - t.IsDelivered = true - - var req *http.Request - var err error - - switch t.HTTPMethod { - case "": - log.Info("HTTP Method for webhook %d empty, setting to POST as default", t.ID) - fallthrough - case http.MethodPost: - switch t.ContentType { - case ContentTypeJSON: - req, err = http.NewRequest("POST", t.URL, strings.NewReader(t.PayloadContent)) - if err != nil { - return err - } - - req.Header.Set("Content-Type", "application/json") - case ContentTypeForm: - var forms = url.Values{ - "payload": []string{t.PayloadContent}, - } - - req, err = http.NewRequest("POST", t.URL, strings.NewReader(forms.Encode())) - if err != nil { - - return err - } - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - } - case http.MethodGet: - u, err := url.Parse(t.URL) - if err != nil { - return err - } - vals := u.Query() - vals["payload"] = []string{t.PayloadContent} - u.RawQuery = vals.Encode() - req, err = http.NewRequest("GET", u.String(), nil) - if err != nil { - return err - } - default: - return fmt.Errorf("Invalid http method for webhook: [%d] %v", t.ID, t.HTTPMethod) - } - - req.Header.Add("X-Gitea-Delivery", t.UUID) - req.Header.Add("X-Gitea-Event", string(t.EventType)) - req.Header.Add("X-Gitea-Signature", t.Signature) - req.Header.Add("X-Gogs-Delivery", t.UUID) - req.Header.Add("X-Gogs-Event", string(t.EventType)) - req.Header.Add("X-Gogs-Signature", t.Signature) - req.Header["X-GitHub-Delivery"] = []string{t.UUID} - req.Header["X-GitHub-Event"] = []string{string(t.EventType)} - - // Record delivery information. - t.RequestInfo = &HookRequest{ - Headers: map[string]string{}, - } - for k, vals := range req.Header { - t.RequestInfo.Headers[k] = strings.Join(vals, ",") - } - - t.ResponseInfo = &HookResponse{ - Headers: map[string]string{}, - } - - defer func() { - t.Delivered = time.Now().UnixNano() - if t.IsSucceed { - log.Trace("Hook delivered: %s", t.UUID) - } else { - log.Trace("Hook delivery failed: %s", t.UUID) - } - - if err := UpdateHookTask(t); err != nil { - log.Error("UpdateHookTask [%d]: %v", t.ID, err) - } - - // Update webhook last delivery status. - w, err := GetWebhookByID(t.HookID) - if err != nil { - log.Error("GetWebhookByID: %v", err) - return - } - if t.IsSucceed { - w.LastStatus = HookStatusSucceed - } else { - w.LastStatus = HookStatusFail - } - if err = UpdateWebhookLastStatus(w); err != nil { - log.Error("UpdateWebhookLastStatus: %v", err) - return - } - }() - - resp, err := webhookHTTPClient.Do(req) - if err != nil { - t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err) - return err - } - defer resp.Body.Close() - - // Status code is 20x can be seen as succeed. - t.IsSucceed = resp.StatusCode/100 == 2 - t.ResponseInfo.Status = resp.StatusCode - for k, vals := range resp.Header { - t.ResponseInfo.Headers[k] = strings.Join(vals, ",") - } - - p, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err) - return err - } - t.ResponseInfo.Body = string(p) - return nil -} - -// DeliverHooks checks and delivers undelivered hooks. -// TODO: shoot more hooks at same time. -func DeliverHooks() { +// FindUndeliveredHookTasks represents find the undelivered hook tasks +func FindUndeliveredHookTasks() ([]*HookTask, error) { tasks := make([]*HookTask, 0, 10) - err := x.Where("is_delivered=?", false).Find(&tasks) - if err != nil { - log.Error("DeliverHooks: %v", err) - return - } - - // Update hook task status. - for _, t := range tasks { - if err = t.deliver(); err != nil { - log.Error("deliver: %v", err) - } - } - - // Start listening on new hook requests. - for repoIDStr := range HookQueue.Queue() { - log.Trace("DeliverHooks [repo_id: %v]", repoIDStr) - HookQueue.Remove(repoIDStr) - - repoID, err := com.StrTo(repoIDStr).Int64() - if err != nil { - log.Error("Invalid repo ID: %s", repoIDStr) - continue - } - - tasks = make([]*HookTask, 0, 5) - if err := x.Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil { - log.Error("Get repository [%d] hook tasks: %v", repoID, err) - continue - } - for _, t := range tasks { - if err = t.deliver(); err != nil { - log.Error("deliver: %v", err) - } - } + if err := x.Where("is_delivered=?", false).Find(&tasks); err != nil { + return nil, err } + return tasks, nil } -var webhookHTTPClient *http.Client - -// InitDeliverHooks starts the hooks delivery thread -func InitDeliverHooks() { - timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second - - webhookHTTPClient = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify}, - Proxy: http.ProxyFromEnvironment, - Dial: func(netw, addr string) (net.Conn, error) { - conn, err := net.DialTimeout(netw, addr, timeout) - if err != nil { - return nil, err - } - - return conn, conn.SetDeadline(time.Now().Add(timeout)) - - }, - }, +// FindRepoUndeliveredHookTasks represents find the undelivered hook tasks of one repository +func FindRepoUndeliveredHookTasks(repoID int64) ([]*HookTask, error) { + tasks := make([]*HookTask, 0, 5) + if err := x.Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil { + return nil, err } - - go DeliverHooks() + return tasks, nil } diff --git a/models/webhook_test.go b/models/webhook_test.go index 343000f5b59c..7bdaadc5ae95 100644 --- a/models/webhook_test.go +++ b/models/webhook_test.go @@ -253,57 +253,3 @@ func TestUpdateHookTask(t *testing.T) { assert.NoError(t, UpdateHookTask(hook)) AssertExistsAndLoadBean(t, hook) } - -func TestPrepareWebhooks(t *testing.T) { - assert.NoError(t, PrepareTestDatabase()) - - repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) - hookTasks := []*HookTask{ - {RepoID: repo.ID, HookID: 1, EventType: HookEventPush}, - } - for _, hookTask := range hookTasks { - AssertNotExistsBean(t, hookTask) - } - assert.NoError(t, PrepareWebhooks(repo, HookEventPush, &api.PushPayload{})) - for _, hookTask := range hookTasks { - AssertExistsAndLoadBean(t, hookTask) - } -} - -func TestPrepareWebhooksBranchFilterMatch(t *testing.T) { - assert.NoError(t, PrepareTestDatabase()) - - repo := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository) - hookTasks := []*HookTask{ - {RepoID: repo.ID, HookID: 4, EventType: HookEventPush}, - } - for _, hookTask := range hookTasks { - AssertNotExistsBean(t, hookTask) - } - // this test also ensures that * doesn't handle / in any special way (like shell would) - assert.NoError(t, PrepareWebhooks(repo, HookEventPush, &api.PushPayload{Ref: "refs/heads/feature/7791"})) - for _, hookTask := range hookTasks { - AssertExistsAndLoadBean(t, hookTask) - } -} - -func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { - assert.NoError(t, PrepareTestDatabase()) - - repo := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository) - hookTasks := []*HookTask{ - {RepoID: repo.ID, HookID: 4, EventType: HookEventPush}, - } - for _, hookTask := range hookTasks { - AssertNotExistsBean(t, hookTask) - } - assert.NoError(t, PrepareWebhooks(repo, HookEventPush, &api.PushPayload{Ref: "refs/heads/fix_weird_bug"})) - - for _, hookTask := range hookTasks { - AssertNotExistsBean(t, hookTask) - } -} - -// TODO TestHookTask_deliver - -// TODO TestDeliverHooks diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index b4629ac56df5..e2874fae730a 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification/base" api "code.gitea.io/gitea/modules/structs" + webhook_module "code.gitea.io/gitea/modules/webhook" ) type webhookNotifier struct { @@ -43,7 +44,7 @@ func (m *webhookNotifier) NotifyIssueClearLabels(doer *models.User, issue *model return } - err = models.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueLabelCleared, Index: issue.Index, PullRequest: issue.PullRequest.APIFormat(), @@ -51,7 +52,7 @@ func (m *webhookNotifier) NotifyIssueClearLabels(doer *models.User, issue *model Sender: doer.APIFormat(), }) } else { - err = models.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ + err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ Action: api.HookIssueLabelCleared, Index: issue.Index, Issue: issue.APIFormat(), @@ -62,7 +63,7 @@ func (m *webhookNotifier) NotifyIssueClearLabels(doer *models.User, issue *model if err != nil { log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) } else { - go models.HookQueue.Add(issue.RepoID) + go webhook_module.HookQueue.Add(issue.RepoID) } } @@ -71,21 +72,21 @@ func (m *webhookNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo mode, _ := models.AccessLevel(doer, repo) // forked webhook - if err := models.PrepareWebhooks(oldRepo, models.HookEventFork, &api.ForkPayload{ + if err := webhook_module.PrepareWebhooks(oldRepo, models.HookEventFork, &api.ForkPayload{ Forkee: oldRepo.APIFormat(oldMode), Repo: repo.APIFormat(mode), Sender: doer.APIFormat(), }); err != nil { log.Error("PrepareWebhooks [repo_id: %d]: %v", oldRepo.ID, err) } else { - go models.HookQueue.Add(oldRepo.ID) + go webhook_module.HookQueue.Add(oldRepo.ID) } u := repo.MustOwner() // Add to hook queue for created repo after session commit. if u.IsOrganization() { - if err := models.PrepareWebhooks(repo, models.HookEventRepository, &api.RepositoryPayload{ + if err := webhook_module.PrepareWebhooks(repo, models.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoCreated, Repository: repo.APIFormat(models.AccessModeOwner), Organization: u.APIFormat(), @@ -93,7 +94,7 @@ func (m *webhookNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo }); err != nil { log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err) } else { - go models.HookQueue.Add(repo.ID) + go webhook_module.HookQueue.Add(repo.ID) } } } @@ -101,7 +102,7 @@ func (m *webhookNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo func (m *webhookNotifier) NotifyCreateRepository(doer *models.User, u *models.User, repo *models.Repository) { // Add to hook queue for created repo after session commit. if u.IsOrganization() { - if err := models.PrepareWebhooks(repo, models.HookEventRepository, &api.RepositoryPayload{ + if err := webhook_module.PrepareWebhooks(repo, models.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoCreated, Repository: repo.APIFormat(models.AccessModeOwner), Organization: u.APIFormat(), @@ -109,7 +110,7 @@ func (m *webhookNotifier) NotifyCreateRepository(doer *models.User, u *models.Us }); err != nil { log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err) } else { - go models.HookQueue.Add(repo.ID) + go webhook_module.HookQueue.Add(repo.ID) } } } @@ -118,7 +119,7 @@ func (m *webhookNotifier) NotifyDeleteRepository(doer *models.User, repo *models u := repo.MustOwner() if u.IsOrganization() { - if err := models.PrepareWebhooks(repo, models.HookEventRepository, &api.RepositoryPayload{ + if err := webhook_module.PrepareWebhooks(repo, models.HookEventRepository, &api.RepositoryPayload{ Action: api.HookRepoDeleted, Repository: repo.APIFormat(models.AccessModeOwner), Organization: u.APIFormat(), @@ -126,7 +127,7 @@ func (m *webhookNotifier) NotifyDeleteRepository(doer *models.User, repo *models }); err != nil { log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err) } - go models.HookQueue.Add(repo.ID) + go webhook_module.HookQueue.Add(repo.ID) } } @@ -151,7 +152,7 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *mo apiPullRequest.Action = api.HookIssueAssigned } // Assignee comment triggers a webhook - if err := models.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, apiPullRequest); err != nil { + if err := webhook_module.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, apiPullRequest); err != nil { log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err) return } @@ -169,13 +170,13 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *mo apiIssue.Action = api.HookIssueAssigned } // Assignee comment triggers a webhook - if err := models.PrepareWebhooks(issue.Repo, models.HookEventIssues, apiIssue); err != nil { + if err := webhook_module.PrepareWebhooks(issue.Repo, models.HookEventIssues, apiIssue); err != nil { log.Error("PrepareWebhooks [is_pull: %v, remove_assignee: %v]: %v", issue.IsPull, removed, err) return } } - go models.HookQueue.Add(issue.RepoID) + go webhook_module.HookQueue.Add(issue.RepoID) } func (m *webhookNotifier) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) { @@ -187,7 +188,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(doer *models.User, issue *model return } issue.PullRequest.Issue = issue - err = models.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueEdited, Index: issue.Index, Changes: &api.ChangesPayload{ @@ -200,7 +201,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(doer *models.User, issue *model Sender: doer.APIFormat(), }) } else { - err = models.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ + err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ Action: api.HookIssueEdited, Index: issue.Index, Changes: &api.ChangesPayload{ @@ -217,7 +218,7 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(doer *models.User, issue *model if err != nil { log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) } else { - go models.HookQueue.Add(issue.RepoID) + go webhook_module.HookQueue.Add(issue.RepoID) } } @@ -241,7 +242,7 @@ func (m *webhookNotifier) NotifyIssueChangeStatus(doer *models.User, issue *mode } else { apiPullRequest.Action = api.HookIssueReOpened } - err = models.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, apiPullRequest) + err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, apiPullRequest) } else { apiIssue := &api.IssuePayload{ Index: issue.Index, @@ -254,18 +255,18 @@ func (m *webhookNotifier) NotifyIssueChangeStatus(doer *models.User, issue *mode } else { apiIssue.Action = api.HookIssueReOpened } - err = models.PrepareWebhooks(issue.Repo, models.HookEventIssues, apiIssue) + err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventIssues, apiIssue) } if err != nil { log.Error("PrepareWebhooks [is_pull: %v, is_closed: %v]: %v", issue.IsPull, isClosed, err) } else { - go models.HookQueue.Add(issue.Repo.ID) + go webhook_module.HookQueue.Add(issue.Repo.ID) } } func (m *webhookNotifier) NotifyNewIssue(issue *models.Issue) { mode, _ := models.AccessLevel(issue.Poster, issue.Repo) - if err := models.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ + if err := webhook_module.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ Action: api.HookIssueOpened, Index: issue.Index, Issue: issue.APIFormat(), @@ -274,7 +275,7 @@ func (m *webhookNotifier) NotifyNewIssue(issue *models.Issue) { }); err != nil { log.Error("PrepareWebhooks: %v", err) } else { - go models.HookQueue.Add(issue.RepoID) + go webhook_module.HookQueue.Add(issue.RepoID) } } @@ -283,7 +284,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(doer *models.User, issue *mod var err error if issue.IsPull { issue.PullRequest.Issue = issue - err = models.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueEdited, Index: issue.Index, Changes: &api.ChangesPayload{ @@ -296,7 +297,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(doer *models.User, issue *mod Sender: doer.APIFormat(), }) } else { - err = models.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ + err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ Action: api.HookIssueEdited, Index: issue.Index, Changes: &api.ChangesPayload{ @@ -312,7 +313,7 @@ func (m *webhookNotifier) NotifyIssueChangeContent(doer *models.User, issue *mod if err != nil { log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) } else { - go models.HookQueue.Add(issue.RepoID) + go webhook_module.HookQueue.Add(issue.RepoID) } } @@ -332,7 +333,7 @@ func (m *webhookNotifier) NotifyUpdateComment(doer *models.User, c *models.Comme } mode, _ := models.AccessLevel(doer, c.Issue.Repo) - if err := models.PrepareWebhooks(c.Issue.Repo, models.HookEventIssueComment, &api.IssueCommentPayload{ + if err := webhook_module.PrepareWebhooks(c.Issue.Repo, models.HookEventIssueComment, &api.IssueCommentPayload{ Action: api.HookIssueCommentEdited, Issue: c.Issue.APIFormat(), Comment: c.APIFormat(), @@ -347,14 +348,14 @@ func (m *webhookNotifier) NotifyUpdateComment(doer *models.User, c *models.Comme }); err != nil { log.Error("PrepareWebhooks [comment_id: %d]: %v", c.ID, err) } else { - go models.HookQueue.Add(c.Issue.Repo.ID) + go webhook_module.HookQueue.Add(c.Issue.Repo.ID) } } func (m *webhookNotifier) NotifyCreateIssueComment(doer *models.User, repo *models.Repository, issue *models.Issue, comment *models.Comment) { mode, _ := models.AccessLevel(doer, repo) - if err := models.PrepareWebhooks(repo, models.HookEventIssueComment, &api.IssueCommentPayload{ + if err := webhook_module.PrepareWebhooks(repo, models.HookEventIssueComment, &api.IssueCommentPayload{ Action: api.HookIssueCommentCreated, Issue: issue.APIFormat(), Comment: comment.APIFormat(), @@ -364,7 +365,7 @@ func (m *webhookNotifier) NotifyCreateIssueComment(doer *models.User, repo *mode }); err != nil { log.Error("PrepareWebhooks [comment_id: %d]: %v", comment.ID, err) } else { - go models.HookQueue.Add(repo.ID) + go webhook_module.HookQueue.Add(repo.ID) } } @@ -385,7 +386,7 @@ func (m *webhookNotifier) NotifyDeleteComment(doer *models.User, comment *models mode, _ := models.AccessLevel(doer, comment.Issue.Repo) - if err := models.PrepareWebhooks(comment.Issue.Repo, models.HookEventIssueComment, &api.IssueCommentPayload{ + if err := webhook_module.PrepareWebhooks(comment.Issue.Repo, models.HookEventIssueComment, &api.IssueCommentPayload{ Action: api.HookIssueCommentDeleted, Issue: comment.Issue.APIFormat(), Comment: comment.APIFormat(), @@ -395,6 +396,6 @@ func (m *webhookNotifier) NotifyDeleteComment(doer *models.User, comment *models }); err != nil { log.Error("PrepareWebhooks [comment_id: %d]: %v", comment.ID, err) } else { - go models.HookQueue.Add(comment.Issue.Repo.ID) + go webhook_module.HookQueue.Add(comment.Issue.Repo.ID) } } diff --git a/modules/repofiles/action.go b/modules/repofiles/action.go index 9467e4fb72f5..addbdb464c62 100644 --- a/modules/repofiles/action.go +++ b/modules/repofiles/action.go @@ -14,6 +14,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/webhook" ) // CommitRepoActionOptions represent options of a new commit action. @@ -112,7 +113,7 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { } defer func() { - go models.HookQueue.Add(repo.ID) + go webhook.HookQueue.Add(repo.ID) }() apiPusher := pusher.APIFormat() @@ -134,7 +135,7 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { if err != nil { log.Error("GetBranchCommitID[%s]: %v", opts.RefFullName, err) } - if err = models.PrepareWebhooks(repo, models.HookEventCreate, &api.CreatePayload{ + if err = webhook.PrepareWebhooks(repo, models.HookEventCreate, &api.CreatePayload{ Ref: refName, Sha: shaSum, RefType: "branch", @@ -148,7 +149,7 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { case models.ActionDeleteBranch: // Delete Branch isHookEventPush = true - if err = models.PrepareWebhooks(repo, models.HookEventDelete, &api.DeletePayload{ + if err = webhook.PrepareWebhooks(repo, models.HookEventDelete, &api.DeletePayload{ Ref: refName, RefType: "branch", PusherType: api.PusherTypeUser, @@ -169,7 +170,7 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { if err != nil { log.Error("GetTagCommitID[%s]: %v", opts.RefFullName, err) } - if err = models.PrepareWebhooks(repo, models.HookEventCreate, &api.CreatePayload{ + if err = webhook.PrepareWebhooks(repo, models.HookEventCreate, &api.CreatePayload{ Ref: refName, Sha: shaSum, RefType: "tag", @@ -181,7 +182,7 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { case models.ActionDeleteTag: // Delete Tag isHookEventPush = true - if err = models.PrepareWebhooks(repo, models.HookEventDelete, &api.DeletePayload{ + if err = webhook.PrepareWebhooks(repo, models.HookEventDelete, &api.DeletePayload{ Ref: refName, RefType: "tag", PusherType: api.PusherTypeUser, @@ -197,7 +198,7 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { if err != nil { return err } - if err = models.PrepareWebhooks(repo, models.HookEventPush, &api.PushPayload{ + if err = webhook.PrepareWebhooks(repo, models.HookEventPush, &api.PushPayload{ Ref: opts.RefFullName, Before: opts.OldCommitID, After: opts.NewCommitID, diff --git a/modules/webhook/deliver.go b/modules/webhook/deliver.go new file mode 100644 index 000000000000..1b3da25d5e8f --- /dev/null +++ b/modules/webhook/deliver.go @@ -0,0 +1,208 @@ +// 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 + +import ( + "crypto/tls" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "strings" + "time" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "github.com/unknwon/com" +) + +// Deliver deliver hook task +func Deliver(t *models.HookTask) error { + t.IsDelivered = true + + var req *http.Request + var err error + + switch t.HTTPMethod { + case "": + log.Info("HTTP Method for webhook %d empty, setting to POST as default", t.ID) + fallthrough + case http.MethodPost: + switch t.ContentType { + case models.ContentTypeJSON: + req, err = http.NewRequest("POST", t.URL, strings.NewReader(t.PayloadContent)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + case models.ContentTypeForm: + var forms = url.Values{ + "payload": []string{t.PayloadContent}, + } + + req, err = http.NewRequest("POST", t.URL, strings.NewReader(forms.Encode())) + if err != nil { + + return err + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + case http.MethodGet: + u, err := url.Parse(t.URL) + if err != nil { + return err + } + vals := u.Query() + vals["payload"] = []string{t.PayloadContent} + u.RawQuery = vals.Encode() + req, err = http.NewRequest("GET", u.String(), nil) + if err != nil { + return err + } + default: + return fmt.Errorf("Invalid http method for webhook: [%d] %v", t.ID, t.HTTPMethod) + } + + req.Header.Add("X-Gitea-Delivery", t.UUID) + req.Header.Add("X-Gitea-Event", string(t.EventType)) + req.Header.Add("X-Gitea-Signature", t.Signature) + req.Header.Add("X-Gogs-Delivery", t.UUID) + req.Header.Add("X-Gogs-Event", string(t.EventType)) + req.Header.Add("X-Gogs-Signature", t.Signature) + req.Header["X-GitHub-Delivery"] = []string{t.UUID} + req.Header["X-GitHub-Event"] = []string{string(t.EventType)} + + // Record delivery information. + t.RequestInfo = &models.HookRequest{ + Headers: map[string]string{}, + } + for k, vals := range req.Header { + t.RequestInfo.Headers[k] = strings.Join(vals, ",") + } + + t.ResponseInfo = &models.HookResponse{ + Headers: map[string]string{}, + } + + defer func() { + t.Delivered = time.Now().UnixNano() + if t.IsSucceed { + log.Trace("Hook delivered: %s", t.UUID) + } else { + log.Trace("Hook delivery failed: %s", t.UUID) + } + + if err := models.UpdateHookTask(t); err != nil { + log.Error("UpdateHookTask [%d]: %v", t.ID, err) + } + + // Update webhook last delivery status. + w, err := models.GetWebhookByID(t.HookID) + if err != nil { + log.Error("GetWebhookByID: %v", err) + return + } + if t.IsSucceed { + w.LastStatus = models.HookStatusSucceed + } else { + w.LastStatus = models.HookStatusFail + } + if err = models.UpdateWebhookLastStatus(w); err != nil { + log.Error("UpdateWebhookLastStatus: %v", err) + return + } + }() + + resp, err := webhookHTTPClient.Do(req) + if err != nil { + t.ResponseInfo.Body = fmt.Sprintf("Delivery: %v", err) + return err + } + defer resp.Body.Close() + + // Status code is 20x can be seen as succeed. + t.IsSucceed = resp.StatusCode/100 == 2 + t.ResponseInfo.Status = resp.StatusCode + for k, vals := range resp.Header { + t.ResponseInfo.Headers[k] = strings.Join(vals, ",") + } + + p, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.ResponseInfo.Body = fmt.Sprintf("read body: %s", err) + return err + } + t.ResponseInfo.Body = string(p) + return nil +} + +// DeliverHooks checks and delivers undelivered hooks. +// TODO: shoot more hooks at same time. +func DeliverHooks() { + tasks, err := models.FindUndeliveredHookTasks() + if err != nil { + log.Error("DeliverHooks: %v", err) + return + } + + // Update hook task status. + for _, t := range tasks { + if err = Deliver(t); err != nil { + log.Error("deliver: %v", err) + } + } + + // Start listening on new hook requests. + for repoIDStr := range HookQueue.Queue() { + log.Trace("DeliverHooks [repo_id: %v]", repoIDStr) + HookQueue.Remove(repoIDStr) + + repoID, err := com.StrTo(repoIDStr).Int64() + if err != nil { + log.Error("Invalid repo ID: %s", repoIDStr) + continue + } + + tasks, err := models.FindRepoUndeliveredHookTasks(repoID) + if err != nil { + log.Error("Get repository [%d] hook tasks: %v", repoID, err) + continue + } + for _, t := range tasks { + if err = Deliver(t); err != nil { + log.Error("deliver: %v", err) + } + } + } +} + +var webhookHTTPClient *http.Client + +// InitDeliverHooks starts the hooks delivery thread +func InitDeliverHooks() { + timeout := time.Duration(setting.Webhook.DeliverTimeout) * time.Second + + webhookHTTPClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: setting.Webhook.SkipTLSVerify}, + Proxy: http.ProxyFromEnvironment, + Dial: func(netw, addr string) (net.Conn, error) { + conn, err := net.DialTimeout(netw, addr, timeout) + if err != nil { + return nil, err + } + + return conn, conn.SetDeadline(time.Now().Add(timeout)) + + }, + }, + } + + go DeliverHooks() +} diff --git a/modules/webhook/main_test.go b/modules/webhook/main_test.go new file mode 100644 index 000000000000..6cb0cffe494a --- /dev/null +++ b/modules/webhook/main_test.go @@ -0,0 +1,16 @@ +// 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 + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models" +) + +func TestMain(m *testing.M) { + models.MainTest(m, filepath.Join("..", "..")) +} diff --git a/modules/webhook/webhook.go b/modules/webhook/webhook.go new file mode 100644 index 000000000000..da88c58c88bc --- /dev/null +++ b/modules/webhook/webhook.go @@ -0,0 +1,179 @@ +// 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 + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/sync" + "github.com/gobwas/glob" +) + +// HookQueue is a global queue of web hooks +var HookQueue = sync.NewUniqueQueue(setting.Webhook.QueueLength) + +// getPayloadBranch returns branch for hook event, if applicable. +func getPayloadBranch(p api.Payloader) string { + switch pp := p.(type) { + case *api.CreatePayload: + if pp.RefType == "branch" { + return pp.Ref + } + case *api.DeletePayload: + if pp.RefType == "branch" { + return pp.Ref + } + case *api.PushPayload: + if strings.HasPrefix(pp.Ref, git.BranchPrefix) { + return pp.Ref[len(git.BranchPrefix):] + } + } + return "" +} + +// PrepareWebhook adds special webhook to task queue for given payload. +func PrepareWebhook(w *models.Webhook, repo *models.Repository, event models.HookEventType, p api.Payloader) error { + return prepareWebhook(w, repo, event, p) +} + +func checkBranch(w *models.Webhook, branch string) bool { + if w.BranchFilter == "" || w.BranchFilter == "*" { + return true + } + + g, err := glob.Compile(w.BranchFilter) + if err != nil { + // should not really happen as BranchFilter is validated + log.Error("CheckBranch failed: %s", err) + return false + } + + return g.Match(branch) +} + +func prepareWebhook(w *models.Webhook, repo *models.Repository, event models.HookEventType, p api.Payloader) error { + for _, e := range w.EventCheckers() { + if event == e.Type { + if !e.Has() { + return nil + } + } + } + + // If payload has no associated branch (e.g. it's a new tag, issue, etc.), + // branch filter has no effect. + if branch := getPayloadBranch(p); branch != "" { + if !checkBranch(w, branch) { + log.Info("Branch %q doesn't match branch filter %q, skipping", branch, w.BranchFilter) + return nil + } + } + + var payloader api.Payloader + var err error + // Use separate objects so modifications won't be made on payload on non-Gogs/Gitea type hooks. + switch w.HookTaskType { + case models.SLACK: + payloader, err = models.GetSlackPayload(p, event, w.Meta) + if err != nil { + return fmt.Errorf("GetSlackPayload: %v", err) + } + case models.DISCORD: + payloader, err = models.GetDiscordPayload(p, event, w.Meta) + if err != nil { + return fmt.Errorf("GetDiscordPayload: %v", err) + } + case models.DINGTALK: + payloader, err = models.GetDingtalkPayload(p, event, w.Meta) + if err != nil { + return fmt.Errorf("GetDingtalkPayload: %v", err) + } + case models.TELEGRAM: + payloader, err = models.GetTelegramPayload(p, event, w.Meta) + if err != nil { + return fmt.Errorf("GetTelegramPayload: %v", err) + } + case models.MSTEAMS: + payloader, err = models.GetMSTeamsPayload(p, event, w.Meta) + if err != nil { + return fmt.Errorf("GetMSTeamsPayload: %v", err) + } + default: + p.SetSecret(w.Secret) + payloader = p + } + + var signature string + if len(w.Secret) > 0 { + data, err := payloader.JSONPayload() + if err != nil { + log.Error("prepareWebhooks.JSONPayload: %v", err) + } + sig := hmac.New(sha256.New, []byte(w.Secret)) + _, err = sig.Write(data) + if err != nil { + log.Error("prepareWebhooks.sigWrite: %v", err) + } + signature = hex.EncodeToString(sig.Sum(nil)) + } + + if err = models.CreateHookTask(&models.HookTask{ + RepoID: repo.ID, + HookID: w.ID, + Type: w.HookTaskType, + URL: w.URL, + Signature: signature, + Payloader: payloader, + HTTPMethod: w.HTTPMethod, + ContentType: w.ContentType, + EventType: event, + IsSSL: w.IsSSL, + }); err != nil { + return fmt.Errorf("CreateHookTask: %v", err) + } + return nil +} + +// PrepareWebhooks adds new webhooks to task queue for given payload. +func PrepareWebhooks(repo *models.Repository, event models.HookEventType, p api.Payloader) error { + return prepareWebhooks(repo, event, p) +} + +func prepareWebhooks(repo *models.Repository, event models.HookEventType, p api.Payloader) error { + ws, err := models.GetActiveWebhooksByRepoID(repo.ID) + if err != nil { + return fmt.Errorf("GetActiveWebhooksByRepoID: %v", err) + } + + // check if repo belongs to org and append additional webhooks + if repo.MustOwner().IsOrganization() { + // get hooks for org + orgHooks, err := models.GetActiveWebhooksByOrgID(repo.OwnerID) + if err != nil { + return fmt.Errorf("GetActiveWebhooksByOrgID: %v", err) + } + ws = append(ws, orgHooks...) + } + + if len(ws) == 0 { + return nil + } + + for _, w := range ws { + if err = prepareWebhook(w, repo, event, p); err != nil { + return err + } + } + return nil +} diff --git a/modules/webhook/webhook_test.go b/modules/webhook/webhook_test.go new file mode 100644 index 000000000000..c944bc477d3b --- /dev/null +++ b/modules/webhook/webhook_test.go @@ -0,0 +1,67 @@ +// 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 + +import ( + "testing" + + "code.gitea.io/gitea/models" + api "code.gitea.io/gitea/modules/structs" + "github.com/stretchr/testify/assert" +) + +func TestPrepareWebhooks(t *testing.T) { + assert.NoError(t, models.PrepareTestDatabase()) + + repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) + hookTasks := []*models.HookTask{ + {RepoID: repo.ID, HookID: 1, EventType: models.HookEventPush}, + } + for _, hookTask := range hookTasks { + models.AssertNotExistsBean(t, hookTask) + } + assert.NoError(t, PrepareWebhooks(repo, models.HookEventPush, &api.PushPayload{})) + for _, hookTask := range hookTasks { + models.AssertExistsAndLoadBean(t, hookTask) + } +} + +func TestPrepareWebhooksBranchFilterMatch(t *testing.T) { + assert.NoError(t, models.PrepareTestDatabase()) + + repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 2}).(*models.Repository) + hookTasks := []*models.HookTask{ + {RepoID: repo.ID, HookID: 4, EventType: models.HookEventPush}, + } + for _, hookTask := range hookTasks { + models.AssertNotExistsBean(t, hookTask) + } + // this test also ensures that * doesn't handle / in any special way (like shell would) + assert.NoError(t, PrepareWebhooks(repo, models.HookEventPush, &api.PushPayload{Ref: "refs/heads/feature/7791"})) + for _, hookTask := range hookTasks { + models.AssertExistsAndLoadBean(t, hookTask) + } +} + +func TestPrepareWebhooksBranchFilterNoMatch(t *testing.T) { + assert.NoError(t, models.PrepareTestDatabase()) + + repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 2}).(*models.Repository) + hookTasks := []*models.HookTask{ + {RepoID: repo.ID, HookID: 4, EventType: models.HookEventPush}, + } + for _, hookTask := range hookTasks { + models.AssertNotExistsBean(t, hookTask) + } + assert.NoError(t, PrepareWebhooks(repo, models.HookEventPush, &api.PushPayload{Ref: "refs/heads/fix_weird_bug"})) + + for _, hookTask := range hookTasks { + models.AssertNotExistsBean(t, hookTask) + } +} + +// TODO TestHookTask_deliver + +// TODO TestDeliverHooks diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index 5c71262560bc..56fc51450817 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/routers/api/v1/convert" "code.gitea.io/gitea/routers/api/v1/utils" ) @@ -122,7 +123,7 @@ func TestHook(ctx *context.APIContext) { return } - if err := models.PrepareWebhook(hook, ctx.Repo.Repository, models.HookEventPush, &api.PushPayload{ + if err := webhook.PrepareWebhook(hook, ctx.Repo.Repository, models.HookEventPush, &api.PushPayload{ Ref: git.BranchPrefix + ctx.Repo.Repository.DefaultBranch, Before: ctx.Repo.Commit.ID.String(), After: ctx.Repo.Commit.ID.String(), @@ -136,7 +137,7 @@ func TestHook(ctx *context.APIContext) { ctx.Error(500, "PrepareWebhook: ", err) return } - go models.HookQueue.Add(ctx.Repo.Repository.ID) + go webhook.HookQueue.Add(ctx.Repo.Repository.ID) ctx.Status(204) } diff --git a/routers/init.go b/routers/init.go index bdd978d717ab..4623f02e6619 100644 --- a/routers/init.go +++ b/routers/init.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/ssh" "code.gitea.io/gitea/modules/task" + "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/mailer" mirror_service "code.gitea.io/gitea/services/mirror" @@ -101,7 +102,7 @@ func GlobalInit() { issue_indexer.InitIssueIndexer(false) models.InitRepoIndexer() mirror_service.InitSyncMirrors() - models.InitDeliverHooks() + webhook.InitDeliverHooks() models.InitTestPullRequests() if err := task.Init(); err != nil { log.Fatal("Failed to initialize task scheduler: %v", err) diff --git a/routers/repo/pull.go b/routers/repo/pull.go index 3defd04b1bd1..d28ee0cfaf39 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/gitdiff" pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" @@ -823,7 +824,7 @@ func TriggerTask(ctx *context.Context) { log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) - go models.HookQueue.Add(repo.ID) + go webhook.HookQueue.Add(repo.ID) go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true) ctx.Status(202) } diff --git a/routers/repo/webhook.go b/routers/repo/webhook.go index 48b4e7afff66..f89bce819081 100644 --- a/routers/repo/webhook.go +++ b/routers/repo/webhook.go @@ -19,6 +19,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/webhook" "github.com/unknwon/com" ) @@ -864,11 +865,11 @@ func TestWebhook(ctx *context.Context) { Pusher: apiUser, Sender: apiUser, } - if err := models.PrepareWebhook(w, ctx.Repo.Repository, models.HookEventPush, p); err != nil { + if err := webhook.PrepareWebhook(w, ctx.Repo.Repository, models.HookEventPush, p); err != nil { ctx.Flash.Error("PrepareWebhook: " + err.Error()) ctx.Status(500) } else { - go models.HookQueue.Add(ctx.Repo.Repository.ID) + go webhook.HookQueue.Add(ctx.Repo.Repository.ID) ctx.Flash.Info(ctx.Tr("repo.settings.webhook.test_delivery_success")) ctx.Status(200) } diff --git a/services/issue/label.go b/services/issue/label.go index b393e5d38c9c..bb36d5849213 100644 --- a/services/issue/label.go +++ b/services/issue/label.go @@ -9,6 +9,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/webhook" ) func sendLabelUpdatedWebhook(issue *models.Issue, doer *models.User) { @@ -34,7 +35,7 @@ func sendLabelUpdatedWebhook(issue *models.Issue, doer *models.User) { log.Error("LoadIssue: %v", err) return } - err = models.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + err = webhook.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueLabelUpdated, Index: issue.Index, PullRequest: issue.PullRequest.APIFormat(), @@ -42,7 +43,7 @@ func sendLabelUpdatedWebhook(issue *models.Issue, doer *models.User) { Sender: doer.APIFormat(), }) } else { - err = models.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ + err = webhook.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ Action: api.HookIssueLabelUpdated, Index: issue.Index, Issue: issue.APIFormat(), @@ -53,7 +54,7 @@ func sendLabelUpdatedWebhook(issue *models.Issue, doer *models.User) { if err != nil { log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) } else { - go models.HookQueue.Add(issue.RepoID) + go webhook.HookQueue.Add(issue.RepoID) } } diff --git a/services/milestone/milestone.go b/services/milestone/milestone.go index 68052e5a6cef..d6d18ad9858b 100644 --- a/services/milestone/milestone.go +++ b/services/milestone/milestone.go @@ -8,6 +8,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/webhook" ) // ChangeMilestoneAssign changes assignment of milestone for issue. @@ -34,7 +35,7 @@ func ChangeMilestoneAssign(issue *models.Issue, doer *models.User, oldMilestoneI log.Error("LoadIssue: %v", err) return } - err = models.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + err = webhook.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ Action: hookAction, Index: issue.Index, PullRequest: issue.PullRequest.APIFormat(), @@ -42,7 +43,7 @@ func ChangeMilestoneAssign(issue *models.Issue, doer *models.User, oldMilestoneI Sender: doer.APIFormat(), }) } else { - err = models.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ + err = webhook.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ Action: hookAction, Index: issue.Index, Issue: issue.APIFormat(), @@ -53,7 +54,7 @@ func ChangeMilestoneAssign(issue *models.Issue, doer *models.User, oldMilestoneI if err != nil { log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) } else { - go models.HookQueue.Add(issue.RepoID) + go webhook.HookQueue.Add(issue.RepoID) } return nil } diff --git a/services/mirror/sync.go b/services/mirror/sync.go index 4bc153b47fe8..1bf36f5760b2 100644 --- a/services/mirror/sync.go +++ b/services/mirror/sync.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/webhook" ) func syncAction(opType models.ActionType, repo *models.Repository, refName string, data []byte) error { @@ -28,7 +29,7 @@ func syncAction(opType models.ActionType, repo *models.Repository, refName strin } defer func() { - go models.HookQueue.Add(repo.ID) + go webhook.HookQueue.Add(repo.ID) }() return nil @@ -55,7 +56,7 @@ func SyncPushAction(repo *models.Repository, opts SyncPushActionOptions) error { opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID) apiPusher := repo.MustOwner().APIFormat() - if err := models.PrepareWebhooks(repo, models.HookEventPush, &api.PushPayload{ + if err := webhook.PrepareWebhooks(repo, models.HookEventPush, &api.PushPayload{ Ref: opts.RefName, Before: opts.OldCommitID, After: opts.NewCommitID, diff --git a/services/pull/merge.go b/services/pull/merge.go index 2e093eef86af..f13638c29249 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -23,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/webhook" "github.com/mcuadros/go-version" ) @@ -360,7 +361,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor } mode, _ := models.AccessLevel(doer, pr.Issue.Repo) - if err = models.PrepareWebhooks(pr.Issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + if err = webhook.PrepareWebhooks(pr.Issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueClosed, Index: pr.Index, PullRequest: pr.APIFormat(), @@ -369,7 +370,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor }); err != nil { log.Error("PrepareWebhooks: %v", err) } else { - go models.HookQueue.Add(pr.Issue.Repo.ID) + go webhook.HookQueue.Add(pr.Issue.Repo.ID) } return nil diff --git a/services/pull/pull.go b/services/pull/pull.go index 8e6110ac3632..65b78f410144 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -11,6 +11,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/webhook" issue_service "code.gitea.io/gitea/services/issue" ) @@ -41,7 +42,7 @@ func NewPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int6 pr.Issue = pull pull.PullRequest = pr mode, _ := models.AccessLevel(pull.Poster, repo) - if err := models.PrepareWebhooks(repo, models.HookEventPullRequest, &api.PullRequestPayload{ + if err := webhook.PrepareWebhooks(repo, models.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueOpened, Index: pull.Index, PullRequest: pr.APIFormat(), @@ -50,7 +51,7 @@ func NewPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int6 }); err != nil { log.Error("PrepareWebhooks: %v", err) } else { - go models.HookQueue.Add(repo.ID) + go webhook.HookQueue.Add(repo.ID) } return nil @@ -114,7 +115,7 @@ func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSy log.Error("LoadAttributes: %v", err) continue } - if err = models.PrepareWebhooks(pr.Issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + if err = webhook.PrepareWebhooks(pr.Issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ Action: api.HookIssueSynchronized, Index: pr.Issue.Index, PullRequest: pr.Issue.PullRequest.APIFormat(), @@ -124,7 +125,7 @@ func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSy log.Error("PrepareWebhooks [pull_id: %v]: %v", pr.ID, err) continue } - go models.HookQueue.Add(pr.Issue.Repo.ID) + go webhook.HookQueue.Add(pr.Issue.Repo.ID) } } diff --git a/services/pull/review.go b/services/pull/review.go index 3388e4bb5630..cffe5da4dd6c 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -8,6 +8,7 @@ package pull import ( "code.gitea.io/gitea/models" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/webhook" ) // CreateReview creates a new review based on opts @@ -55,7 +56,7 @@ func reviewHook(review *models.Review) error { if err != nil { return err } - if err := models.PrepareWebhooks(review.Issue.Repo, reviewHookType, &api.PullRequestPayload{ + if err := webhook.PrepareWebhooks(review.Issue.Repo, reviewHookType, &api.PullRequestPayload{ Action: api.HookIssueSynchronized, Index: review.Issue.Index, PullRequest: pr.APIFormat(), @@ -68,7 +69,7 @@ func reviewHook(review *models.Review) error { }); err != nil { return err } - go models.HookQueue.Add(review.Issue.Repo.ID) + go webhook.HookQueue.Add(review.Issue.Repo.ID) return nil } diff --git a/services/release/release.go b/services/release/release.go index b4f814a77f39..42db1f341745 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/modules/process" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/webhook" ) func createTag(gitRepo *git.Repository, rel *models.Release) error { @@ -84,7 +85,7 @@ func CreateRelease(gitRepo *git.Repository, rel *models.Release, attachmentUUIDs log.Error("LoadAttributes: %v", err) } else { mode, _ := models.AccessLevel(rel.Publisher, rel.Repo) - if err := models.PrepareWebhooks(rel.Repo, models.HookEventRelease, &api.ReleasePayload{ + if err := webhook.PrepareWebhooks(rel.Repo, models.HookEventRelease, &api.ReleasePayload{ Action: api.HookReleasePublished, Release: rel.APIFormat(), Repository: rel.Repo.APIFormat(mode), @@ -92,7 +93,7 @@ func CreateRelease(gitRepo *git.Repository, rel *models.Release, attachmentUUIDs }); err != nil { log.Error("PrepareWebhooks: %v", err) } else { - go models.HookQueue.Add(rel.Repo.ID) + go webhook.HookQueue.Add(rel.Repo.ID) } } } @@ -121,7 +122,7 @@ func UpdateRelease(doer *models.User, gitRepo *git.Repository, rel *models.Relea // even if attachments added failed, hooks will be still triggered mode, _ := models.AccessLevel(doer, rel.Repo) - if err1 := models.PrepareWebhooks(rel.Repo, models.HookEventRelease, &api.ReleasePayload{ + if err1 := webhook.PrepareWebhooks(rel.Repo, models.HookEventRelease, &api.ReleasePayload{ Action: api.HookReleaseUpdated, Release: rel.APIFormat(), Repository: rel.Repo.APIFormat(mode), @@ -129,7 +130,7 @@ func UpdateRelease(doer *models.User, gitRepo *git.Repository, rel *models.Relea }); err1 != nil { log.Error("PrepareWebhooks: %v", err) } else { - go models.HookQueue.Add(rel.Repo.ID) + go webhook.HookQueue.Add(rel.Repo.ID) } return err @@ -187,7 +188,7 @@ func DeleteReleaseByID(id int64, doer *models.User, delTag bool) error { } mode, _ := models.AccessLevel(doer, rel.Repo) - if err := models.PrepareWebhooks(rel.Repo, models.HookEventRelease, &api.ReleasePayload{ + if err := webhook.PrepareWebhooks(rel.Repo, models.HookEventRelease, &api.ReleasePayload{ Action: api.HookReleaseDeleted, Release: rel.APIFormat(), Repository: rel.Repo.APIFormat(mode), @@ -195,7 +196,7 @@ func DeleteReleaseByID(id int64, doer *models.User, delTag bool) error { }); err != nil { log.Error("PrepareWebhooks: %v", err) } else { - go models.HookQueue.Add(rel.Repo.ID) + go webhook.HookQueue.Add(rel.Repo.ID) } return nil From 484edb75836acdc0ea9c5e16737f0f1815c0a2b7 Mon Sep 17 00:00:00 2001 From: May Date: Sat, 2 Nov 2019 01:26:21 +0100 Subject: [PATCH 05/14] Project files table style update (#8757) * add marking to title_wip_desc Signed-off-by: May * update table styling Signed-off-by: May * Update file table column width to 4:9:3 Signed-off-by: May --- templates/repo/view_list.tmpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/repo/view_list.tmpl b/templates/repo/view_list.tmpl index 4c96109e8bf9..87f613f739f9 100644 --- a/templates/repo/view_list.tmpl +++ b/templates/repo/view_list.tmpl @@ -1,4 +1,4 @@ - +
{{end}} - - + {{end}} From 4b8d9e58c58b8952e86e7e5f90a0a8e0480e1de1 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 2 Nov 2019 09:49:57 +0800 Subject: [PATCH 06/14] Move labels webhooks to notification (#8749) * Move webhooks to notification * fix notification --- modules/notification/webhook/webhook.go | 47 +++++++++++++++++++++ services/issue/label.go | 55 ++----------------------- 2 files changed, 50 insertions(+), 52 deletions(-) diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index e2874fae730a..8d98ff0f9ecb 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -399,3 +399,50 @@ func (m *webhookNotifier) NotifyDeleteComment(doer *models.User, comment *models go webhook_module.HookQueue.Add(comment.Issue.Repo.ID) } } + +func (m *webhookNotifier) NotifyIssueChangeLabels(doer *models.User, issue *models.Issue, + addedLabels []*models.Label, removedLabels []*models.Label) { + var err error + + if err = issue.LoadRepo(); err != nil { + log.Error("LoadRepo: %v", err) + return + } + + if err = issue.LoadPoster(); err != nil { + log.Error("LoadPoster: %v", err) + return + } + + mode, _ := models.AccessLevel(issue.Poster, issue.Repo) + if issue.IsPull { + if err = issue.LoadPullRequest(); err != nil { + log.Error("loadPullRequest: %v", err) + return + } + if err = issue.PullRequest.LoadIssue(); err != nil { + log.Error("LoadIssue: %v", err) + return + } + err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + Action: api.HookIssueLabelUpdated, + Index: issue.Index, + PullRequest: issue.PullRequest.APIFormat(), + Repository: issue.Repo.APIFormat(models.AccessModeNone), + Sender: doer.APIFormat(), + }) + } else { + err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ + Action: api.HookIssueLabelUpdated, + Index: issue.Index, + Issue: issue.APIFormat(), + Repository: issue.Repo.APIFormat(mode), + Sender: doer.APIFormat(), + }) + } + if err != nil { + log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) + } else { + go webhook_module.HookQueue.Add(issue.RepoID) + } +} diff --git a/services/issue/label.go b/services/issue/label.go index bb36d5849213..5e154df959dc 100644 --- a/services/issue/label.go +++ b/services/issue/label.go @@ -6,58 +6,9 @@ package issue import ( "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/notification" - api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/webhook" ) -func sendLabelUpdatedWebhook(issue *models.Issue, doer *models.User) { - var err error - - if err = issue.LoadRepo(); err != nil { - log.Error("LoadRepo: %v", err) - return - } - - if err = issue.LoadPoster(); err != nil { - log.Error("LoadPoster: %v", err) - return - } - - mode, _ := models.AccessLevel(issue.Poster, issue.Repo) - if issue.IsPull { - if err = issue.LoadPullRequest(); err != nil { - log.Error("loadPullRequest: %v", err) - return - } - if err = issue.PullRequest.LoadIssue(); err != nil { - log.Error("LoadIssue: %v", err) - return - } - err = webhook.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ - Action: api.HookIssueLabelUpdated, - Index: issue.Index, - PullRequest: issue.PullRequest.APIFormat(), - Repository: issue.Repo.APIFormat(models.AccessModeNone), - Sender: doer.APIFormat(), - }) - } else { - err = webhook.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ - Action: api.HookIssueLabelUpdated, - Index: issue.Index, - Issue: issue.APIFormat(), - Repository: issue.Repo.APIFormat(mode), - Sender: doer.APIFormat(), - }) - } - if err != nil { - log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) - } else { - go webhook.HookQueue.Add(issue.RepoID) - } -} - // ClearLabels clears all of an issue's labels func ClearLabels(issue *models.Issue, doer *models.User) (err error) { if err = issue.ClearLabels(doer); err != nil { @@ -75,7 +26,7 @@ func AddLabel(issue *models.Issue, doer *models.User, label *models.Label) error return err } - sendLabelUpdatedWebhook(issue, doer) + notification.NotifyIssueChangeLabels(doer, issue, []*models.Label{label}, nil) return nil } @@ -85,7 +36,7 @@ func AddLabels(issue *models.Issue, doer *models.User, labels []*models.Label) e return err } - sendLabelUpdatedWebhook(issue, doer) + notification.NotifyIssueChangeLabels(doer, issue, labels, nil) return nil } @@ -107,6 +58,6 @@ func RemoveLabel(issue *models.Issue, doer *models.User, label *models.Label) er return err } - sendLabelUpdatedWebhook(issue, doer) + notification.NotifyIssueChangeLabels(doer, issue, nil, []*models.Label{label}) return nil } From f518fe66620bcb60a5251befca116f8caa48ee42 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 2 Nov 2019 10:35:12 +0800 Subject: [PATCH 07/14] Rename HookQueue to hookQueue (#8778) * Rename HookQueue to hookQueue * fix lint --- modules/notification/webhook/webhook.go | 27 ------------------------- modules/repofiles/action.go | 4 ---- modules/webhook/deliver.go | 4 ++-- modules/webhook/webhook.go | 18 +++++++++++++---- routers/api/v1/repo/hook.go | 2 +- routers/repo/pull.go | 2 -- routers/repo/webhook.go | 1 - services/milestone/milestone.go | 2 -- services/mirror/sync.go | 4 ---- services/pull/merge.go | 2 -- services/pull/pull.go | 3 --- services/pull/review.go | 9 ++------- services/release/release.go | 6 ------ 13 files changed, 19 insertions(+), 65 deletions(-) diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index 8d98ff0f9ecb..259b6352ab24 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -62,8 +62,6 @@ func (m *webhookNotifier) NotifyIssueClearLabels(doer *models.User, issue *model } if err != nil { log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) - } else { - go webhook_module.HookQueue.Add(issue.RepoID) } } @@ -78,8 +76,6 @@ func (m *webhookNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo Sender: doer.APIFormat(), }); err != nil { log.Error("PrepareWebhooks [repo_id: %d]: %v", oldRepo.ID, err) - } else { - go webhook_module.HookQueue.Add(oldRepo.ID) } u := repo.MustOwner() @@ -93,8 +89,6 @@ func (m *webhookNotifier) NotifyForkRepository(doer *models.User, oldRepo, repo Sender: doer.APIFormat(), }); err != nil { log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err) - } else { - go webhook_module.HookQueue.Add(repo.ID) } } } @@ -109,8 +103,6 @@ func (m *webhookNotifier) NotifyCreateRepository(doer *models.User, u *models.Us Sender: doer.APIFormat(), }); err != nil { log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err) - } else { - go webhook_module.HookQueue.Add(repo.ID) } } } @@ -127,7 +119,6 @@ func (m *webhookNotifier) NotifyDeleteRepository(doer *models.User, repo *models }); err != nil { log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err) } - go webhook_module.HookQueue.Add(repo.ID) } } @@ -175,8 +166,6 @@ func (m *webhookNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *mo return } } - - go webhook_module.HookQueue.Add(issue.RepoID) } func (m *webhookNotifier) NotifyIssueChangeTitle(doer *models.User, issue *models.Issue, oldTitle string) { @@ -217,8 +206,6 @@ func (m *webhookNotifier) NotifyIssueChangeTitle(doer *models.User, issue *model if err != nil { log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) - } else { - go webhook_module.HookQueue.Add(issue.RepoID) } } @@ -259,8 +246,6 @@ func (m *webhookNotifier) NotifyIssueChangeStatus(doer *models.User, issue *mode } if err != nil { log.Error("PrepareWebhooks [is_pull: %v, is_closed: %v]: %v", issue.IsPull, isClosed, err) - } else { - go webhook_module.HookQueue.Add(issue.Repo.ID) } } @@ -274,8 +259,6 @@ func (m *webhookNotifier) NotifyNewIssue(issue *models.Issue) { Sender: issue.Poster.APIFormat(), }); err != nil { log.Error("PrepareWebhooks: %v", err) - } else { - go webhook_module.HookQueue.Add(issue.RepoID) } } @@ -312,8 +295,6 @@ func (m *webhookNotifier) NotifyIssueChangeContent(doer *models.User, issue *mod } if err != nil { log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) - } else { - go webhook_module.HookQueue.Add(issue.RepoID) } } @@ -347,8 +328,6 @@ func (m *webhookNotifier) NotifyUpdateComment(doer *models.User, c *models.Comme IsPull: c.Issue.IsPull, }); err != nil { log.Error("PrepareWebhooks [comment_id: %d]: %v", c.ID, err) - } else { - go webhook_module.HookQueue.Add(c.Issue.Repo.ID) } } @@ -364,8 +343,6 @@ func (m *webhookNotifier) NotifyCreateIssueComment(doer *models.User, repo *mode IsPull: issue.IsPull, }); err != nil { log.Error("PrepareWebhooks [comment_id: %d]: %v", comment.ID, err) - } else { - go webhook_module.HookQueue.Add(repo.ID) } } @@ -395,8 +372,6 @@ func (m *webhookNotifier) NotifyDeleteComment(doer *models.User, comment *models IsPull: comment.Issue.IsPull, }); err != nil { log.Error("PrepareWebhooks [comment_id: %d]: %v", comment.ID, err) - } else { - go webhook_module.HookQueue.Add(comment.Issue.Repo.ID) } } @@ -442,7 +417,5 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(doer *models.User, issue *mode } if err != nil { log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) - } else { - go webhook_module.HookQueue.Add(issue.RepoID) } } diff --git a/modules/repofiles/action.go b/modules/repofiles/action.go index addbdb464c62..79f6406c535d 100644 --- a/modules/repofiles/action.go +++ b/modules/repofiles/action.go @@ -112,10 +112,6 @@ func CommitRepoAction(opts CommitRepoActionOptions) error { return fmt.Errorf("NotifyWatchers: %v", err) } - defer func() { - go webhook.HookQueue.Add(repo.ID) - }() - apiPusher := pusher.APIFormat() apiRepo := repo.APIFormat(models.AccessModeNone) diff --git a/modules/webhook/deliver.go b/modules/webhook/deliver.go index 1b3da25d5e8f..54f20171fad6 100644 --- a/modules/webhook/deliver.go +++ b/modules/webhook/deliver.go @@ -159,9 +159,9 @@ func DeliverHooks() { } // Start listening on new hook requests. - for repoIDStr := range HookQueue.Queue() { + for repoIDStr := range hookQueue.Queue() { log.Trace("DeliverHooks [repo_id: %v]", repoIDStr) - HookQueue.Remove(repoIDStr) + hookQueue.Remove(repoIDStr) repoID, err := com.StrTo(repoIDStr).Int64() if err != nil { diff --git a/modules/webhook/webhook.go b/modules/webhook/webhook.go index da88c58c88bc..623a475df9b7 100644 --- a/modules/webhook/webhook.go +++ b/modules/webhook/webhook.go @@ -20,8 +20,8 @@ import ( "github.com/gobwas/glob" ) -// HookQueue is a global queue of web hooks -var HookQueue = sync.NewUniqueQueue(setting.Webhook.QueueLength) +// hookQueue is a global queue of web hooks +var hookQueue = sync.NewUniqueQueue(setting.Webhook.QueueLength) // getPayloadBranch returns branch for hook event, if applicable. func getPayloadBranch(p api.Payloader) string { @@ -44,7 +44,12 @@ func getPayloadBranch(p api.Payloader) string { // PrepareWebhook adds special webhook to task queue for given payload. func PrepareWebhook(w *models.Webhook, repo *models.Repository, event models.HookEventType, p api.Payloader) error { - return prepareWebhook(w, repo, event, p) + if err := prepareWebhook(w, repo, event, p); err != nil { + return err + } + + go hookQueue.Add(repo.ID) + return nil } func checkBranch(w *models.Webhook, branch string) bool { @@ -147,7 +152,12 @@ func prepareWebhook(w *models.Webhook, repo *models.Repository, event models.Hoo // PrepareWebhooks adds new webhooks to task queue for given payload. func PrepareWebhooks(repo *models.Repository, event models.HookEventType, p api.Payloader) error { - return prepareWebhooks(repo, event, p) + if err := prepareWebhooks(repo, event, p); err != nil { + return err + } + + go hookQueue.Add(repo.ID) + return nil } func prepareWebhooks(repo *models.Repository, event models.HookEventType, p api.Payloader) error { diff --git a/routers/api/v1/repo/hook.go b/routers/api/v1/repo/hook.go index 56fc51450817..18f1fba0562c 100644 --- a/routers/api/v1/repo/hook.go +++ b/routers/api/v1/repo/hook.go @@ -137,7 +137,7 @@ func TestHook(ctx *context.APIContext) { ctx.Error(500, "PrepareWebhook: ", err) return } - go webhook.HookQueue.Add(ctx.Repo.Repository.ID) + ctx.Status(204) } diff --git a/routers/repo/pull.go b/routers/repo/pull.go index d28ee0cfaf39..cb9c7c116406 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -23,7 +23,6 @@ import ( "code.gitea.io/gitea/modules/notification" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/util" - "code.gitea.io/gitea/modules/webhook" "code.gitea.io/gitea/services/gitdiff" pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" @@ -824,7 +823,6 @@ func TriggerTask(ctx *context.Context) { log.Trace("TriggerTask '%s/%s' by %s", repo.Name, branch, pusher.Name) - go webhook.HookQueue.Add(repo.ID) go pull_service.AddTestPullRequestTask(pusher, repo.ID, branch, true) ctx.Status(202) } diff --git a/routers/repo/webhook.go b/routers/repo/webhook.go index f89bce819081..a6bd3af264a3 100644 --- a/routers/repo/webhook.go +++ b/routers/repo/webhook.go @@ -869,7 +869,6 @@ func TestWebhook(ctx *context.Context) { ctx.Flash.Error("PrepareWebhook: " + err.Error()) ctx.Status(500) } else { - go webhook.HookQueue.Add(ctx.Repo.Repository.ID) ctx.Flash.Info(ctx.Tr("repo.settings.webhook.test_delivery_success")) ctx.Status(200) } diff --git a/services/milestone/milestone.go b/services/milestone/milestone.go index d6d18ad9858b..84e7fb30e29d 100644 --- a/services/milestone/milestone.go +++ b/services/milestone/milestone.go @@ -53,8 +53,6 @@ func ChangeMilestoneAssign(issue *models.Issue, doer *models.User, oldMilestoneI } if err != nil { log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) - } else { - go webhook.HookQueue.Add(issue.RepoID) } return nil } diff --git a/services/mirror/sync.go b/services/mirror/sync.go index 1bf36f5760b2..a9ce189c0301 100644 --- a/services/mirror/sync.go +++ b/services/mirror/sync.go @@ -28,10 +28,6 @@ func syncAction(opType models.ActionType, repo *models.Repository, refName strin return fmt.Errorf("notifyWatchers: %v", err) } - defer func() { - go webhook.HookQueue.Add(repo.ID) - }() - return nil } diff --git a/services/pull/merge.go b/services/pull/merge.go index f13638c29249..4a2f4511c447 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -369,8 +369,6 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor Sender: doer.APIFormat(), }); err != nil { log.Error("PrepareWebhooks: %v", err) - } else { - go webhook.HookQueue.Add(pr.Issue.Repo.ID) } return nil diff --git a/services/pull/pull.go b/services/pull/pull.go index 65b78f410144..0a4c4a7eeeca 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -50,8 +50,6 @@ func NewPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int6 Sender: pull.Poster.APIFormat(), }); err != nil { log.Error("PrepareWebhooks: %v", err) - } else { - go webhook.HookQueue.Add(repo.ID) } return nil @@ -125,7 +123,6 @@ func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSy log.Error("PrepareWebhooks [pull_id: %v]: %v", pr.ID, err) continue } - go webhook.HookQueue.Add(pr.Issue.Repo.ID) } } diff --git a/services/pull/review.go b/services/pull/review.go index cffe5da4dd6c..ffb7be82b2ee 100644 --- a/services/pull/review.go +++ b/services/pull/review.go @@ -56,7 +56,7 @@ func reviewHook(review *models.Review) error { if err != nil { return err } - if err := webhook.PrepareWebhooks(review.Issue.Repo, reviewHookType, &api.PullRequestPayload{ + return webhook.PrepareWebhooks(review.Issue.Repo, reviewHookType, &api.PullRequestPayload{ Action: api.HookIssueSynchronized, Index: review.Issue.Index, PullRequest: pr.APIFormat(), @@ -66,10 +66,5 @@ func reviewHook(review *models.Review) error { Type: string(reviewHookType), Content: review.Content, }, - }); err != nil { - return err - } - go webhook.HookQueue.Add(review.Issue.Repo.ID) - - return nil + }) } diff --git a/services/release/release.go b/services/release/release.go index 42db1f341745..a3f027c94986 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -92,8 +92,6 @@ func CreateRelease(gitRepo *git.Repository, rel *models.Release, attachmentUUIDs Sender: rel.Publisher.APIFormat(), }); err != nil { log.Error("PrepareWebhooks: %v", err) - } else { - go webhook.HookQueue.Add(rel.Repo.ID) } } } @@ -129,8 +127,6 @@ func UpdateRelease(doer *models.User, gitRepo *git.Repository, rel *models.Relea Sender: doer.APIFormat(), }); err1 != nil { log.Error("PrepareWebhooks: %v", err) - } else { - go webhook.HookQueue.Add(rel.Repo.ID) } return err @@ -195,8 +191,6 @@ func DeleteReleaseByID(id int64, doer *models.User, delTag bool) error { Sender: doer.APIFormat(), }); err != nil { log.Error("PrepareWebhooks: %v", err) - } else { - go webhook.HookQueue.Add(rel.Repo.ID) } return nil From 8f26397928b33a16558dafc2716a72b6e6900bf4 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 2 Nov 2019 11:33:20 +0800 Subject: [PATCH 08/14] Move issue milestone assign to issue service and move webhook to notification (#8780) --- modules/notification/base/notifier.go | 2 +- modules/notification/base/null.go | 2 +- modules/notification/notification.go | 4 +- modules/notification/webhook/webhook.go | 42 ++++++++++++++++++ routers/api/v1/repo/issue.go | 3 +- routers/api/v1/repo/pull.go | 3 +- routers/repo/issue.go | 3 +- services/issue/milestone.go | 21 +++++++++ services/milestone/milestone.go | 58 ------------------------- 9 files changed, 70 insertions(+), 68 deletions(-) create mode 100644 services/issue/milestone.go delete mode 100644 services/milestone/milestone.go diff --git a/modules/notification/base/notifier.go b/modules/notification/base/notifier.go index c74bb52014df..b0e7b4cae827 100644 --- a/modules/notification/base/notifier.go +++ b/modules/notification/base/notifier.go @@ -20,7 +20,7 @@ type Notifier interface { NotifyNewIssue(*models.Issue) NotifyIssueChangeStatus(*models.User, *models.Issue, bool) - NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue) + NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue, oldMilestoneID int64) NotifyIssueChangeAssignee(doer *models.User, issue *models.Issue, assignee *models.User, removed bool, comment *models.Comment) NotifyIssueChangeContent(doer *models.User, issue *models.Issue, oldContent string) NotifyIssueClearLabels(doer *models.User, issue *models.Issue) diff --git a/modules/notification/base/null.go b/modules/notification/base/null.go index 9fb08884a198..3524a53c1554 100644 --- a/modules/notification/base/null.go +++ b/modules/notification/base/null.go @@ -75,7 +75,7 @@ func (*NullNotifier) NotifyDeleteRelease(doer *models.User, rel *models.Release) } // NotifyIssueChangeMilestone places a place holder function -func (*NullNotifier) NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue) { +func (*NullNotifier) NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue, oldMilestoneID int64) { } // NotifyIssueChangeContent places a place holder function diff --git a/modules/notification/notification.go b/modules/notification/notification.go index 0f1b63cf672b..70b1541e3525 100644 --- a/modules/notification/notification.go +++ b/modules/notification/notification.go @@ -128,9 +128,9 @@ func NotifyDeleteRelease(doer *models.User, rel *models.Release) { } // NotifyIssueChangeMilestone notifies change milestone to notifiers -func NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue) { +func NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue, oldMilestoneID int64) { for _, notifier := range notifiers { - notifier.NotifyIssueChangeMilestone(doer, issue) + notifier.NotifyIssueChangeMilestone(doer, issue, oldMilestoneID) } } diff --git a/modules/notification/webhook/webhook.go b/modules/notification/webhook/webhook.go index 259b6352ab24..a969ad74f793 100644 --- a/modules/notification/webhook/webhook.go +++ b/modules/notification/webhook/webhook.go @@ -419,3 +419,45 @@ func (m *webhookNotifier) NotifyIssueChangeLabels(doer *models.User, issue *mode log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) } } + +func (m *webhookNotifier) NotifyIssueChangeMilestone(doer *models.User, issue *models.Issue, oldMilestoneID int64) { + var hookAction api.HookIssueAction + var err error + if issue.MilestoneID > 0 { + hookAction = api.HookIssueMilestoned + } else { + hookAction = api.HookIssueDemilestoned + } + + if err = issue.LoadAttributes(); err != nil { + log.Error("issue.LoadAttributes failed: %v", err) + return + } + + mode, _ := models.AccessLevel(doer, issue.Repo) + if issue.IsPull { + err = issue.PullRequest.LoadIssue() + if err != nil { + log.Error("LoadIssue: %v", err) + return + } + err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ + Action: hookAction, + Index: issue.Index, + PullRequest: issue.PullRequest.APIFormat(), + Repository: issue.Repo.APIFormat(mode), + Sender: doer.APIFormat(), + }) + } else { + err = webhook_module.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ + Action: hookAction, + Index: issue.Index, + Issue: issue.APIFormat(), + Repository: issue.Repo.APIFormat(mode), + Sender: doer.APIFormat(), + }) + } + if err != nil { + log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) + } +} diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go index fe5862ea5e45..1534c45df0a0 100644 --- a/routers/api/v1/repo/issue.go +++ b/routers/api/v1/repo/issue.go @@ -20,7 +20,6 @@ import ( "code.gitea.io/gitea/modules/timeutil" "code.gitea.io/gitea/modules/util" issue_service "code.gitea.io/gitea/services/issue" - milestone_service "code.gitea.io/gitea/services/milestone" ) // SearchIssues searches for issues across the repositories that the user has access to @@ -494,7 +493,7 @@ func EditIssue(ctx *context.APIContext, form api.EditIssueOption) { issue.MilestoneID != *form.Milestone { oldMilestoneID := issue.MilestoneID issue.MilestoneID = *form.Milestone - if err = milestone_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil { + if err = issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil { ctx.Error(500, "ChangeMilestoneAssign", err) return } diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go index 0180aebf89dd..9264c00cec61 100644 --- a/routers/api/v1/repo/pull.go +++ b/routers/api/v1/repo/pull.go @@ -18,7 +18,6 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" issue_service "code.gitea.io/gitea/services/issue" - milestone_service "code.gitea.io/gitea/services/milestone" pull_service "code.gitea.io/gitea/services/pull" ) @@ -420,7 +419,7 @@ func EditPullRequest(ctx *context.APIContext, form api.EditPullRequestOption) { issue.MilestoneID != form.Milestone { oldMilestoneID := issue.MilestoneID issue.MilestoneID = form.Milestone - if err = milestone_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil { + if err = issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil { ctx.Error(500, "ChangeMilestoneAssign", err) return } diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 9a691471d54d..739370da6927 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -28,7 +28,6 @@ import ( "code.gitea.io/gitea/modules/util" comment_service "code.gitea.io/gitea/services/comments" issue_service "code.gitea.io/gitea/services/issue" - milestone_service "code.gitea.io/gitea/services/milestone" "github.com/unknwon/com" ) @@ -1099,7 +1098,7 @@ func UpdateIssueMilestone(ctx *context.Context) { continue } issue.MilestoneID = milestoneID - if err := milestone_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil { + if err := issue_service.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil { ctx.ServerError("ChangeMilestoneAssign", err) return } diff --git a/services/issue/milestone.go b/services/issue/milestone.go new file mode 100644 index 000000000000..6fe527f58cd4 --- /dev/null +++ b/services/issue/milestone.go @@ -0,0 +1,21 @@ +// 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 issue + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/notification" +) + +// ChangeMilestoneAssign changes assignment of milestone for issue. +func ChangeMilestoneAssign(issue *models.Issue, doer *models.User, oldMilestoneID int64) (err error) { + if err = models.ChangeMilestoneAssign(issue, doer, oldMilestoneID); err != nil { + return + } + + notification.NotifyIssueChangeMilestone(doer, issue, oldMilestoneID) + + return nil +} diff --git a/services/milestone/milestone.go b/services/milestone/milestone.go deleted file mode 100644 index 84e7fb30e29d..000000000000 --- a/services/milestone/milestone.go +++ /dev/null @@ -1,58 +0,0 @@ -// 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 models - -import ( - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/log" - api "code.gitea.io/gitea/modules/structs" - "code.gitea.io/gitea/modules/webhook" -) - -// ChangeMilestoneAssign changes assignment of milestone for issue. -func ChangeMilestoneAssign(issue *models.Issue, doer *models.User, oldMilestoneID int64) (err error) { - if err = models.ChangeMilestoneAssign(issue, doer, oldMilestoneID); err != nil { - return - } - - var hookAction api.HookIssueAction - if issue.MilestoneID > 0 { - hookAction = api.HookIssueMilestoned - } else { - hookAction = api.HookIssueDemilestoned - } - - if err = issue.LoadAttributes(); err != nil { - return err - } - - mode, _ := models.AccessLevel(doer, issue.Repo) - if issue.IsPull { - err = issue.PullRequest.LoadIssue() - if err != nil { - log.Error("LoadIssue: %v", err) - return - } - err = webhook.PrepareWebhooks(issue.Repo, models.HookEventPullRequest, &api.PullRequestPayload{ - Action: hookAction, - Index: issue.Index, - PullRequest: issue.PullRequest.APIFormat(), - Repository: issue.Repo.APIFormat(mode), - Sender: doer.APIFormat(), - }) - } else { - err = webhook.PrepareWebhooks(issue.Repo, models.HookEventIssues, &api.IssuePayload{ - Action: hookAction, - Index: issue.Index, - Issue: issue.APIFormat(), - Repository: issue.Repo.APIFormat(mode), - Sender: doer.APIFormat(), - }) - } - if err != nil { - log.Error("PrepareWebhooks [is_pull: %v]: %v", issue.IsPull, err) - } - return nil -} From 9d663dfde68f4f3fd6af302aad47439adb1076ae Mon Sep 17 00:00:00 2001 From: zeripath Date: Sat, 2 Nov 2019 05:40:49 +0000 Subject: [PATCH 09/14] On windows set core.longpaths true (#8776) --- modules/git/git.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/modules/git/git.go b/modules/git/git.go index 964760dfda79..df50eac72a3e 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -8,6 +8,7 @@ package git import ( "fmt" "os/exec" + "runtime" "strings" "time" @@ -133,6 +134,13 @@ func Init() error { return fmt.Errorf("Failed to execute 'git config --global gc.writeCommitGraph true': %s", stderr) } } + + if runtime.GOOS == "windows" { + if _, stderr, err := process.GetManager().Exec("git.Init(git config --global core.longpaths true)", + GitExecutable, "config", "--global", "core.longpaths", "true"); err != nil { + return fmt.Errorf("Failed to execute 'git config --global core.longpaths true': %s", stderr) + } + } return nil } From 232340f5e3ebe61787021bebea01fd755513c72b Mon Sep 17 00:00:00 2001 From: David Svantesson Date: Sat, 2 Nov 2019 08:37:05 +0100 Subject: [PATCH 10/14] Prevent upload (overwrite) of lfs locked file (#8769) * Check if file is locked on upload file commit. * Better user message if file is locked. * Check lfs lock before creating temporary repository. fix some errors. * move lines * Add comment that enabled setting is checked. --- models/error.go | 17 +++++++++++++++++ modules/repofiles/upload.go | 24 +++++++++++++++++------- options/locale/locale_en-US.ini | 1 + routers/repo/editor.go | 6 +++++- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/models/error.go b/models/error.go index 995617e83b8a..505df2886858 100644 --- a/models/error.go +++ b/models/error.go @@ -647,6 +647,23 @@ func (err ErrLFSLockAlreadyExist) Error() string { return fmt.Sprintf("lfs lock already exists [rid: %d, path: %s]", err.RepoID, err.Path) } +// ErrLFSFileLocked represents a "LFSFileLocked" kind of error. +type ErrLFSFileLocked struct { + RepoID int64 + Path string + UserName string +} + +// IsErrLFSFileLocked checks if an error is a ErrLFSFileLocked. +func IsErrLFSFileLocked(err error) bool { + _, ok := err.(ErrLFSFileLocked) + return ok +} + +func (err ErrLFSFileLocked) Error() string { + return fmt.Sprintf("File is lfs locked [repo: %d, locked by: %s, path: %s]", err.RepoID, err.UserName, err.Path) +} + // __________ .__ __ // \______ \ ____ ______ ____ _____|__|/ |_ ___________ ___.__. // | _// __ \\____ \ / _ \/ ___/ \ __\/ _ \_ __ < | | diff --git a/modules/repofiles/upload.go b/modules/repofiles/upload.go index a2e7cc927cb8..eb1379560dff 100644 --- a/modules/repofiles/upload.go +++ b/modules/repofiles/upload.go @@ -55,6 +55,23 @@ func UploadRepoFiles(repo *models.Repository, doer *models.User, opts *UploadRep return fmt.Errorf("GetUploadsByUUIDs [uuids: %v]: %v", opts.Files, err) } + names := make([]string, len(uploads)) + infos := make([]uploadInfo, len(uploads)) + for i, upload := range uploads { + // Check file is not lfs locked, will return nil if lock setting not enabled + filepath := path.Join(opts.TreePath, upload.Name) + lfsLock, err := repo.GetTreePathLock(filepath) + if err != nil { + return err + } + if lfsLock != nil && lfsLock.OwnerID != doer.ID { + return models.ErrLFSFileLocked{RepoID: repo.ID, Path: filepath, UserName: lfsLock.Owner.Name} + } + + names[i] = upload.Name + infos[i] = uploadInfo{upload: upload} + } + t, err := NewTemporaryUploadRepository(repo) if err != nil { return err @@ -67,13 +84,6 @@ func UploadRepoFiles(repo *models.Repository, doer *models.User, opts *UploadRep return err } - names := make([]string, len(uploads)) - infos := make([]uploadInfo, len(uploads)) - for i, upload := range uploads { - names[i] = upload.Name - infos[i] = uploadInfo{upload: upload} - } - var filename2attribute2info map[string]map[string]string if setting.LFS.StartServer { filename2attribute2info, err = t.CheckAttribute("filter", names...) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 4d1af69db53f..794d78c6f7a9 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -742,6 +742,7 @@ editor.no_changes_to_show = There are no changes to show. editor.fail_to_update_file = Failed to update/create file '%s' with error: %v editor.add_subdir = Add a directory… editor.unable_to_upload_files = Failed to upload files to '%s' with error: %v +editor.upload_file_is_locked = File '%s' is locked by %s. editor.upload_files_to_dir = Upload files to '%s' editor.cannot_commit_to_protected_branch = Cannot commit to protected branch '%s'. diff --git a/routers/repo/editor.go b/routers/repo/editor.go index d4a7dab07481..763429f8cffc 100644 --- a/routers/repo/editor.go +++ b/routers/repo/editor.go @@ -585,7 +585,11 @@ func UploadFilePost(ctx *context.Context, form auth.UploadRepoFileForm) { Files: form.Files, }); err != nil { ctx.Data["Err_TreePath"] = true - ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form) + if models.IsErrLFSFileLocked(err) { + ctx.RenderWithErr(ctx.Tr("repo.editor.upload_file_is_locked", err.(models.ErrLFSFileLocked).Path, err.(models.ErrLFSFileLocked).UserName), tplUploadFile, &form) + } else { + ctx.RenderWithErr(ctx.Tr("repo.editor.unable_to_upload_files", form.TreePath, err), tplUploadFile, &form) + } return } From 21dfe1b2567f666789a09c2b38114f389ebac30d Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sat, 2 Nov 2019 21:38:11 +0800 Subject: [PATCH 11/14] fix 500 when edit hook (#8782) --- modules/git/hook.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/modules/git/hook.go b/modules/git/hook.go index e9665916687b..245c6b92cecb 100644 --- a/modules/git/hook.go +++ b/modules/git/hook.go @@ -90,6 +90,11 @@ func (h *Hook) Update() error { h.IsActive = false return nil } + d := filepath.Dir(h.path) + if err := os.MkdirAll(d, os.ModePerm); err != nil { + return err + } + err := ioutil.WriteFile(h.path, []byte(strings.Replace(h.Content, "\r", "", -1)), os.ModePerm) if err != nil { return err From dd9cb32bff1a001d3f18cf4f5fcbfd1a0c3a2570 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 2 Nov 2019 13:40:49 +0000 Subject: [PATCH 12/14] [skip ci] Updated translations via Crowdin --- options/locale/locale_de-DE.ini | 2 ++ 1 file changed, 2 insertions(+) diff --git a/options/locale/locale_de-DE.ini b/options/locale/locale_de-DE.ini index 3b4ae38f5a68..a37d9d7dd79a 100644 --- a/options/locale/locale_de-DE.ini +++ b/options/locale/locale_de-DE.ini @@ -741,6 +741,7 @@ editor.no_changes_to_show=Keine Änderungen vorhanden. editor.fail_to_update_file=Fehler beim Ändern/Erstellen der Datei „%s“. Fehler: %v editor.add_subdir=Verzeichnis erstellen… editor.unable_to_upload_files=Fehler beim Hochladen der Dateien nach „%s“. Fehler: %v +editor.upload_file_is_locked=Datei „%s” ist durch %s gesperrt. editor.upload_files_to_dir=Dateien hochladen nach „%s“ editor.cannot_commit_to_protected_branch=Commit in den geschützten Branch „%s“ ist nicht möglich. @@ -1392,6 +1393,7 @@ settings.lfs_pointers.oid=OID settings.lfs_pointers.inRepo=Im Repo settings.lfs_pointers.exists=Existiert im Speicher settings.lfs_pointers.accessible=Nutzer hat Zugriff +settings.lfs_pointers.associateAccessible=Ordne %d zugängliche OIDs zu diff.browse_source=Quellcode durchsuchen diff.parent=Ursprung From 5a187f4bcc9cbe5f44da4b623046289c97ede250 Mon Sep 17 00:00:00 2001 From: 6543 <24977596+6543@users.noreply.github.com> Date: Sat, 2 Nov 2019 16:27:49 +0100 Subject: [PATCH 13/14] Add API for Issue set Subscription (#8729) * add issue subscriber API * subscribers return []user.APIFormat * add comments * more meaningfull description * without "reqToken()" api works ... * should be still secure beause ctx.user has to be there or nothing will hapen * FIX: getIssueWatchers() get only aktive suscriber * add return avter error on right position * Revert "FIX: getIssueWatchers() get only aktive suscriber" This reverts commit 5eca9291858a821981992b0aaa38cef610d84bca. * Update routers/api/v1/repo/issue.go Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * test go linter again * update swagger * GetIssueWatchers -> GetIssueSubscribers part one Co-Authored-By: guillep2k <18600385+guillep2k@users.noreply.github.com> * GetIssueWatchers -> GetIssueSubscribers part two * Revert "test go linter again" This reverts commit bab12356227e44334de113b76f12099de0b8aaa6. * change description for unsubscribe too * golangci-lint timeout avter 5min * move issueSubscription to seperate file * dont create black entitys * use IsWatching until refactoring * Update License Info * better swagger description * Update .golangci.yml because functions moved from issue.go to issue_subscription.go * add IssueWatchList type * batch tasks * use e Engien * add error handling * error should be the last type when returning multiple items * short version * reurn empy UserList instead of nil --- .golangci.yml | 3 + Makefile | 2 +- models/issue_watch.go | 33 +++- models/userlist.go | 10 + routers/api/v1/api.go | 5 + routers/api/v1/repo/issue_subscription.go | 216 ++++++++++++++++++++++ templates/swagger/v1_json.tmpl | 159 ++++++++++++++++ 7 files changed, 425 insertions(+), 3 deletions(-) create mode 100644 routers/api/v1/repo/issue_subscription.go diff --git a/.golangci.yml b/.golangci.yml index fd7393372be4..121ef583accb 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -73,6 +73,9 @@ issues: - path: routers/routes/routes.go linters: - dupl + - path: routers/api/v1/repo/issue_subscription.go + linters: + - dupl - path: routers/repo/view.go linters: - dupl diff --git a/Makefile b/Makefile index ebcfadb21db6..34685dea93c3 100644 --- a/Makefile +++ b/Makefile @@ -521,4 +521,4 @@ golangci-lint: export BINARY="golangci-lint"; \ curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.20.0; \ fi - golangci-lint run + golangci-lint run --timeout 5m diff --git a/models/issue_watch.go b/models/issue_watch.go index 2f55c6a84db8..1ae0c9d47471 100644 --- a/models/issue_watch.go +++ b/models/issue_watch.go @@ -16,6 +16,9 @@ type IssueWatch struct { UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"` } +// IssueWatchList contains IssueWatch +type IssueWatchList []*IssueWatch + // CreateOrUpdateIssueWatch set watching for a user and issue func CreateOrUpdateIssueWatch(userID, issueID int64, isWatching bool) error { iw, exists, err := getIssueWatch(x, userID, issueID) @@ -58,11 +61,11 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool } // GetIssueWatchers returns watchers/unwatchers of a given issue -func GetIssueWatchers(issueID int64) ([]*IssueWatch, error) { +func GetIssueWatchers(issueID int64) (IssueWatchList, error) { return getIssueWatchers(x, issueID) } -func getIssueWatchers(e Engine, issueID int64) (watches []*IssueWatch, err error) { +func getIssueWatchers(e Engine, issueID int64) (watches IssueWatchList, err error) { err = e. Where("`issue_watch`.issue_id = ?", issueID). And("`user`.is_active = ?", true). @@ -83,3 +86,29 @@ func removeIssueWatchersByRepoID(e Engine, userID int64, repoID int64) error { Update(iw) return err } + +// LoadWatchUsers return watching users +func (iwl IssueWatchList) LoadWatchUsers() (users UserList, err error) { + return iwl.loadWatchUsers(x) +} + +func (iwl IssueWatchList) loadWatchUsers(e Engine) (users UserList, err error) { + if len(iwl) == 0 { + return []*User{}, nil + } + + var userIDs = make([]int64, 0, len(iwl)) + for _, iw := range iwl { + if iw.IsWatching { + userIDs = append(userIDs, iw.UserID) + } + } + + if len(userIDs) == 0 { + return []*User{}, nil + } + + err = e.In("id", userIDs).Find(&users) + + return +} diff --git a/models/userlist.go b/models/userlist.go index a2a424848227..16ec6fee55a3 100644 --- a/models/userlist.go +++ b/models/userlist.go @@ -8,6 +8,7 @@ import ( "fmt" "code.gitea.io/gitea/modules/log" + api "code.gitea.io/gitea/modules/structs" ) //UserList is a list of user. @@ -93,3 +94,12 @@ func (users UserList) loadTwoFactorStatus(e Engine) (map[int64]*TwoFactor, error } return tokenMaps, nil } + +//APIFormat return list of users in api format +func (users UserList) APIFormat() []*api.User { + var result []*api.User + for _, u := range users { + result = append(result, u.APIFormat()) + } + return result +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 1acd849b8d84..b68717f7c875 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -690,6 +690,11 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/start", reqToken(), repo.StartIssueStopwatch) m.Post("/stop", reqToken(), repo.StopIssueStopwatch) }) + m.Group("/subscriptions", func() { + m.Get("", bind(api.User{}), repo.GetIssueSubscribers) + m.Put("/:user", repo.AddIssueSubscription) + m.Delete("/:user", repo.DelIssueSubscription) + }) }) }, mustEnableIssuesOrPulls) m.Group("/labels", func() { diff --git a/routers/api/v1/repo/issue_subscription.go b/routers/api/v1/repo/issue_subscription.go new file mode 100644 index 000000000000..012dcda44fa8 --- /dev/null +++ b/routers/api/v1/repo/issue_subscription.go @@ -0,0 +1,216 @@ +// 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 repo + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/context" + api "code.gitea.io/gitea/modules/structs" +) + +// AddIssueSubscription Subscribe user to issue +func AddIssueSubscription(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/issues/{index}/subscriptions/{user} issue issueAddSubscription + // --- + // summary: Subscribe user to issue + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the issue + // type: integer + // format: int64 + // required: true + // - name: user + // in: path + // description: user to subscribe + // type: string + // required: true + // responses: + // "201": + // "$ref": "#/responses/empty" + // "304": + // description: User can only subscribe itself if he is no admin + // "404": + // description: Issue not found + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if models.IsErrIssueNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(500, "GetIssueByIndex", err) + } + + return + } + + user, err := models.GetUserByName(ctx.Params(":user")) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(500, "GetUserByName", err) + } + + return + } + + //only admin and user for itself can change subscription + if user.ID != ctx.User.ID && !ctx.User.IsAdmin { + ctx.Error(403, "User", nil) + return + } + + if err := models.CreateOrUpdateIssueWatch(user.ID, issue.ID, true); err != nil { + ctx.Error(500, "CreateOrUpdateIssueWatch", err) + return + } + + ctx.Status(201) +} + +// DelIssueSubscription Unsubscribe user from issue +func DelIssueSubscription(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/subscriptions/{user} issue issueDeleteSubscription + // --- + // summary: Unsubscribe user from issue + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the issue + // type: integer + // format: int64 + // required: true + // - name: user + // in: path + // description: user witch unsubscribe + // type: string + // required: true + // responses: + // "201": + // "$ref": "#/responses/empty" + // "304": + // description: User can only subscribe itself if he is no admin + // "404": + // description: Issue not found + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if models.IsErrIssueNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(500, "GetIssueByIndex", err) + } + + return + } + + user, err := models.GetUserByName(ctx.Params(":user")) + if err != nil { + if models.IsErrUserNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(500, "GetUserByName", err) + } + + return + } + + //only admin and user for itself can change subscription + if user.ID != ctx.User.ID && !ctx.User.IsAdmin { + ctx.Error(403, "User", nil) + return + } + + if err := models.CreateOrUpdateIssueWatch(user.ID, issue.ID, false); err != nil { + ctx.Error(500, "CreateOrUpdateIssueWatch", err) + return + } + + ctx.Status(201) +} + +// GetIssueSubscribers return subscribers of an issue +func GetIssueSubscribers(ctx *context.APIContext, form api.User) { + // swagger:operation GET /repos/{owner}/{repo}/issues/{index}/subscriptions issue issueSubscriptions + // --- + // summary: Get users who subscribed on an issue. + // consumes: + // - application/json + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // - name: index + // in: path + // description: index of the issue + // type: integer + // format: int64 + // required: true + // responses: + // "201": + // "$ref": "#/responses/empty" + // "404": + // description: Issue not found + issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index")) + if err != nil { + if models.IsErrIssueNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(500, "GetIssueByIndex", err) + } + + return + } + + iwl, err := models.GetIssueWatchers(issue.ID) + if err != nil { + ctx.Error(500, "GetIssueWatchers", err) + return + } + + users, err := iwl.LoadWatchUsers() + if err != nil { + ctx.Error(500, "LoadWatchUsers", err) + return + } + + ctx.JSON(200, users.APIFormat()) +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index da7ebda8523a..41e5353ea73b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -3781,6 +3781,165 @@ } } }, + "/repos/{owner}/{repo}/issues/{index}/subscriptions": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Get users who subscribed on an issue.", + "operationId": "issueSubscriptions", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "index of the issue", + "name": "index", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "$ref": "#/responses/empty" + }, + "404": { + "description": "Issue not found" + } + } + } + }, + "/repos/{owner}/{repo}/issues/{index}/subscriptions/{user}": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Subscribe user to issue", + "operationId": "issueAddSubscription", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "index of the issue", + "name": "index", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "user to subscribe", + "name": "user", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "$ref": "#/responses/empty" + }, + "304": { + "description": "User can only subscribe itself if he is no admin" + }, + "404": { + "description": "Issue not found" + } + } + }, + "delete": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "issue" + ], + "summary": "Unsubscribe user from issue", + "operationId": "issueDeleteSubscription", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "index of the issue", + "name": "index", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "user witch unsubscribe", + "name": "user", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "$ref": "#/responses/empty" + }, + "304": { + "description": "User can only subscribe itself if he is no admin" + }, + "404": { + "description": "Issue not found" + } + } + } + }, "/repos/{owner}/{repo}/keys": { "get": { "produces": [ From 729708e878f543cbe1aef3908cfde67bf77a07eb Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sat, 2 Nov 2019 15:29:18 +0000 Subject: [PATCH 14/14] [skip ci] Updated translations via Crowdin --- options/locale/locale_pt-BR.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/options/locale/locale_pt-BR.ini b/options/locale/locale_pt-BR.ini index 762000edfcd9..4ff0684f31df 100644 --- a/options/locale/locale_pt-BR.ini +++ b/options/locale/locale_pt-BR.ini @@ -741,6 +741,7 @@ editor.no_changes_to_show=Nenhuma alteração a mostrar. editor.fail_to_update_file=Houve erro ao criar ou atualizar arquivo '%s': %v editor.add_subdir=Adicionar um subdiretório... editor.unable_to_upload_files=Houve erro ao fazer upload de arquivos para '%s': %v +editor.upload_file_is_locked=Arquivo '%s' está bloqueado por %s. editor.upload_files_to_dir=Enviar arquivos para '%s' editor.cannot_commit_to_protected_branch=Branch '%s' está protegido para commits.
@@ -62,7 +62,7 @@ {{else}} - + {{if $entry.IsDir}} {{$subJumpablePathName := $entry.GetSubJumpablePathName}} @@ -82,12 +82,12 @@ + {{$commit.Summary}} {{TimeSince $commit.Committer.When $.Lang}}{{TimeSince $commit.Committer.When $.Lang}}