You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

webhook.go 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2017 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package models
  6. import (
  7. "encoding/json"
  8. "fmt"
  9. "time"
  10. "code.gitea.io/gitea/modules/log"
  11. "code.gitea.io/gitea/modules/setting"
  12. api "code.gitea.io/gitea/modules/structs"
  13. "code.gitea.io/gitea/modules/timeutil"
  14. gouuid "github.com/satori/go.uuid"
  15. )
  16. // HookContentType is the content type of a web hook
  17. type HookContentType int
  18. const (
  19. // ContentTypeJSON is a JSON payload for web hooks
  20. ContentTypeJSON HookContentType = iota + 1
  21. // ContentTypeForm is an url-encoded form payload for web hook
  22. ContentTypeForm
  23. )
  24. var hookContentTypes = map[string]HookContentType{
  25. "json": ContentTypeJSON,
  26. "form": ContentTypeForm,
  27. }
  28. // ToHookContentType returns HookContentType by given name.
  29. func ToHookContentType(name string) HookContentType {
  30. return hookContentTypes[name]
  31. }
  32. // Name returns the name of a given web hook's content type
  33. func (t HookContentType) Name() string {
  34. switch t {
  35. case ContentTypeJSON:
  36. return "json"
  37. case ContentTypeForm:
  38. return "form"
  39. }
  40. return ""
  41. }
  42. // IsValidHookContentType returns true if given name is a valid hook content type.
  43. func IsValidHookContentType(name string) bool {
  44. _, ok := hookContentTypes[name]
  45. return ok
  46. }
  47. // HookEvents is a set of web hook events
  48. type HookEvents struct {
  49. Create bool `json:"create"`
  50. Delete bool `json:"delete"`
  51. Fork bool `json:"fork"`
  52. Issues bool `json:"issues"`
  53. IssueComment bool `json:"issue_comment"`
  54. Push bool `json:"push"`
  55. PullRequest bool `json:"pull_request"`
  56. Repository bool `json:"repository"`
  57. Release bool `json:"release"`
  58. }
  59. // HookEvent represents events that will delivery hook.
  60. type HookEvent struct {
  61. PushOnly bool `json:"push_only"`
  62. SendEverything bool `json:"send_everything"`
  63. ChooseEvents bool `json:"choose_events"`
  64. BranchFilter string `json:"branch_filter"`
  65. HookEvents `json:"events"`
  66. }
  67. // HookStatus is the status of a web hook
  68. type HookStatus int
  69. // Possible statuses of a web hook
  70. const (
  71. HookStatusNone = iota
  72. HookStatusSucceed
  73. HookStatusFail
  74. )
  75. // Webhook represents a web hook object.
  76. type Webhook struct {
  77. ID int64 `xorm:"pk autoincr"`
  78. RepoID int64 `xorm:"INDEX"`
  79. OrgID int64 `xorm:"INDEX"`
  80. URL string `xorm:"url TEXT"`
  81. Signature string `xorm:"TEXT"`
  82. HTTPMethod string `xorm:"http_method"`
  83. ContentType HookContentType
  84. Secret string `xorm:"TEXT"`
  85. Events string `xorm:"TEXT"`
  86. *HookEvent `xorm:"-"`
  87. IsSSL bool `xorm:"is_ssl"`
  88. IsActive bool `xorm:"INDEX"`
  89. HookTaskType HookTaskType
  90. Meta string `xorm:"TEXT"` // store hook-specific attributes
  91. LastStatus HookStatus // Last delivery status
  92. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  93. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  94. }
  95. // AfterLoad updates the webhook object upon setting a column
  96. func (w *Webhook) AfterLoad() {
  97. w.HookEvent = &HookEvent{}
  98. if err := json.Unmarshal([]byte(w.Events), w.HookEvent); err != nil {
  99. log.Error("Unmarshal[%d]: %v", w.ID, err)
  100. }
  101. }
  102. // GetSlackHook returns slack metadata
  103. func (w *Webhook) GetSlackHook() *SlackMeta {
  104. s := &SlackMeta{}
  105. if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
  106. log.Error("webhook.GetSlackHook(%d): %v", w.ID, err)
  107. }
  108. return s
  109. }
  110. // GetDiscordHook returns discord metadata
  111. func (w *Webhook) GetDiscordHook() *DiscordMeta {
  112. s := &DiscordMeta{}
  113. if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
  114. log.Error("webhook.GetDiscordHook(%d): %v", w.ID, err)
  115. }
  116. return s
  117. }
  118. // GetTelegramHook returns telegram metadata
  119. func (w *Webhook) GetTelegramHook() *TelegramMeta {
  120. s := &TelegramMeta{}
  121. if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
  122. log.Error("webhook.GetTelegramHook(%d): %v", w.ID, err)
  123. }
  124. return s
  125. }
  126. // History returns history of webhook by given conditions.
  127. func (w *Webhook) History(page int) ([]*HookTask, error) {
  128. return HookTasks(w.ID, page)
  129. }
  130. // UpdateEvent handles conversion from HookEvent to Events.
  131. func (w *Webhook) UpdateEvent() error {
  132. data, err := json.Marshal(w.HookEvent)
  133. w.Events = string(data)
  134. return err
  135. }
  136. // HasCreateEvent returns true if hook enabled create event.
  137. func (w *Webhook) HasCreateEvent() bool {
  138. return w.SendEverything ||
  139. (w.ChooseEvents && w.HookEvents.Create)
  140. }
  141. // HasDeleteEvent returns true if hook enabled delete event.
  142. func (w *Webhook) HasDeleteEvent() bool {
  143. return w.SendEverything ||
  144. (w.ChooseEvents && w.HookEvents.Delete)
  145. }
  146. // HasForkEvent returns true if hook enabled fork event.
  147. func (w *Webhook) HasForkEvent() bool {
  148. return w.SendEverything ||
  149. (w.ChooseEvents && w.HookEvents.Fork)
  150. }
  151. // HasIssuesEvent returns true if hook enabled issues event.
  152. func (w *Webhook) HasIssuesEvent() bool {
  153. return w.SendEverything ||
  154. (w.ChooseEvents && w.HookEvents.Issues)
  155. }
  156. // HasIssueCommentEvent returns true if hook enabled issue_comment event.
  157. func (w *Webhook) HasIssueCommentEvent() bool {
  158. return w.SendEverything ||
  159. (w.ChooseEvents && w.HookEvents.IssueComment)
  160. }
  161. // HasPushEvent returns true if hook enabled push event.
  162. func (w *Webhook) HasPushEvent() bool {
  163. return w.PushOnly || w.SendEverything ||
  164. (w.ChooseEvents && w.HookEvents.Push)
  165. }
  166. // HasPullRequestEvent returns true if hook enabled pull request event.
  167. func (w *Webhook) HasPullRequestEvent() bool {
  168. return w.SendEverything ||
  169. (w.ChooseEvents && w.HookEvents.PullRequest)
  170. }
  171. // HasReleaseEvent returns if hook enabled release event.
  172. func (w *Webhook) HasReleaseEvent() bool {
  173. return w.SendEverything ||
  174. (w.ChooseEvents && w.HookEvents.Release)
  175. }
  176. // HasRepositoryEvent returns if hook enabled repository event.
  177. func (w *Webhook) HasRepositoryEvent() bool {
  178. return w.SendEverything ||
  179. (w.ChooseEvents && w.HookEvents.Repository)
  180. }
  181. // EventCheckers returns event checkers
  182. func (w *Webhook) EventCheckers() []struct {
  183. Has func() bool
  184. Type HookEventType
  185. } {
  186. return []struct {
  187. Has func() bool
  188. Type HookEventType
  189. }{
  190. {w.HasCreateEvent, HookEventCreate},
  191. {w.HasDeleteEvent, HookEventDelete},
  192. {w.HasForkEvent, HookEventFork},
  193. {w.HasPushEvent, HookEventPush},
  194. {w.HasIssuesEvent, HookEventIssues},
  195. {w.HasIssueCommentEvent, HookEventIssueComment},
  196. {w.HasPullRequestEvent, HookEventPullRequest},
  197. {w.HasRepositoryEvent, HookEventRepository},
  198. {w.HasReleaseEvent, HookEventRelease},
  199. }
  200. }
  201. // EventsArray returns an array of hook events
  202. func (w *Webhook) EventsArray() []string {
  203. events := make([]string, 0, 7)
  204. for _, c := range w.EventCheckers() {
  205. if c.Has() {
  206. events = append(events, string(c.Type))
  207. }
  208. }
  209. return events
  210. }
  211. // CreateWebhook creates a new web hook.
  212. func CreateWebhook(w *Webhook) error {
  213. return createWebhook(x, w)
  214. }
  215. func createWebhook(e Engine, w *Webhook) error {
  216. _, err := e.Insert(w)
  217. return err
  218. }
  219. // getWebhook uses argument bean as query condition,
  220. // ID must be specified and do not assign unnecessary fields.
  221. func getWebhook(bean *Webhook) (*Webhook, error) {
  222. has, err := x.Get(bean)
  223. if err != nil {
  224. return nil, err
  225. } else if !has {
  226. return nil, ErrWebhookNotExist{bean.ID}
  227. }
  228. return bean, nil
  229. }
  230. // GetWebhookByID returns webhook of repository by given ID.
  231. func GetWebhookByID(id int64) (*Webhook, error) {
  232. return getWebhook(&Webhook{
  233. ID: id,
  234. })
  235. }
  236. // GetWebhookByRepoID returns webhook of repository by given ID.
  237. func GetWebhookByRepoID(repoID, id int64) (*Webhook, error) {
  238. return getWebhook(&Webhook{
  239. ID: id,
  240. RepoID: repoID,
  241. })
  242. }
  243. // GetWebhookByOrgID returns webhook of organization by given ID.
  244. func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) {
  245. return getWebhook(&Webhook{
  246. ID: id,
  247. OrgID: orgID,
  248. })
  249. }
  250. // GetActiveWebhooksByRepoID returns all active webhooks of repository.
  251. func GetActiveWebhooksByRepoID(repoID int64) ([]*Webhook, error) {
  252. return getActiveWebhooksByRepoID(x, repoID)
  253. }
  254. func getActiveWebhooksByRepoID(e Engine, repoID int64) ([]*Webhook, error) {
  255. webhooks := make([]*Webhook, 0, 5)
  256. return webhooks, e.Where("is_active=?", true).
  257. Find(&webhooks, &Webhook{RepoID: repoID})
  258. }
  259. // GetWebhooksByRepoID returns all webhooks of a repository.
  260. func GetWebhooksByRepoID(repoID int64) ([]*Webhook, error) {
  261. webhooks := make([]*Webhook, 0, 5)
  262. return webhooks, x.Find(&webhooks, &Webhook{RepoID: repoID})
  263. }
  264. // GetActiveWebhooksByOrgID returns all active webhooks for an organization.
  265. func GetActiveWebhooksByOrgID(orgID int64) (ws []*Webhook, err error) {
  266. return getActiveWebhooksByOrgID(x, orgID)
  267. }
  268. func getActiveWebhooksByOrgID(e Engine, orgID int64) (ws []*Webhook, err error) {
  269. err = e.
  270. Where("org_id=?", orgID).
  271. And("is_active=?", true).
  272. Find(&ws)
  273. return ws, err
  274. }
  275. // GetWebhooksByOrgID returns all webhooks for an organization.
  276. func GetWebhooksByOrgID(orgID int64) (ws []*Webhook, err error) {
  277. err = x.Find(&ws, &Webhook{OrgID: orgID})
  278. return ws, err
  279. }
  280. // GetDefaultWebhook returns admin-default webhook by given ID.
  281. func GetDefaultWebhook(id int64) (*Webhook, error) {
  282. webhook := &Webhook{ID: id}
  283. has, err := x.
  284. Where("repo_id=? AND org_id=?", 0, 0).
  285. Get(webhook)
  286. if err != nil {
  287. return nil, err
  288. } else if !has {
  289. return nil, ErrWebhookNotExist{id}
  290. }
  291. return webhook, nil
  292. }
  293. // GetDefaultWebhooks returns all admin-default webhooks.
  294. func GetDefaultWebhooks() ([]*Webhook, error) {
  295. return getDefaultWebhooks(x)
  296. }
  297. func getDefaultWebhooks(e Engine) ([]*Webhook, error) {
  298. webhooks := make([]*Webhook, 0, 5)
  299. return webhooks, e.
  300. Where("repo_id=? AND org_id=?", 0, 0).
  301. Find(&webhooks)
  302. }
  303. // UpdateWebhook updates information of webhook.
  304. func UpdateWebhook(w *Webhook) error {
  305. _, err := x.ID(w.ID).AllCols().Update(w)
  306. return err
  307. }
  308. // UpdateWebhookLastStatus updates last status of webhook.
  309. func UpdateWebhookLastStatus(w *Webhook) error {
  310. _, err := x.ID(w.ID).Cols("last_status").Update(w)
  311. return err
  312. }
  313. // deleteWebhook uses argument bean as query condition,
  314. // ID must be specified and do not assign unnecessary fields.
  315. func deleteWebhook(bean *Webhook) (err error) {
  316. sess := x.NewSession()
  317. defer sess.Close()
  318. if err = sess.Begin(); err != nil {
  319. return err
  320. }
  321. if count, err := sess.Delete(bean); err != nil {
  322. return err
  323. } else if count == 0 {
  324. return ErrWebhookNotExist{ID: bean.ID}
  325. } else if _, err = sess.Delete(&HookTask{HookID: bean.ID}); err != nil {
  326. return err
  327. }
  328. return sess.Commit()
  329. }
  330. // DeleteWebhookByRepoID deletes webhook of repository by given ID.
  331. func DeleteWebhookByRepoID(repoID, id int64) error {
  332. return deleteWebhook(&Webhook{
  333. ID: id,
  334. RepoID: repoID,
  335. })
  336. }
  337. // DeleteWebhookByOrgID deletes webhook of organization by given ID.
  338. func DeleteWebhookByOrgID(orgID, id int64) error {
  339. return deleteWebhook(&Webhook{
  340. ID: id,
  341. OrgID: orgID,
  342. })
  343. }
  344. // DeleteDefaultWebhook deletes an admin-default webhook by given ID.
  345. func DeleteDefaultWebhook(id int64) error {
  346. sess := x.NewSession()
  347. defer sess.Close()
  348. if err := sess.Begin(); err != nil {
  349. return err
  350. }
  351. count, err := sess.
  352. Where("repo_id=? AND org_id=?", 0, 0).
  353. Delete(&Webhook{ID: id})
  354. if err != nil {
  355. return err
  356. } else if count == 0 {
  357. return ErrWebhookNotExist{ID: id}
  358. }
  359. if _, err := sess.Delete(&HookTask{HookID: id}); err != nil {
  360. return err
  361. }
  362. return sess.Commit()
  363. }
  364. // copyDefaultWebhooksToRepo creates copies of the default webhooks in a new repo
  365. func copyDefaultWebhooksToRepo(e Engine, repoID int64) error {
  366. ws, err := getDefaultWebhooks(e)
  367. if err != nil {
  368. return fmt.Errorf("GetDefaultWebhooks: %v", err)
  369. }
  370. for _, w := range ws {
  371. w.ID = 0
  372. w.RepoID = repoID
  373. if err := createWebhook(e, w); err != nil {
  374. return fmt.Errorf("CreateWebhook: %v", err)
  375. }
  376. }
  377. return nil
  378. }
  379. // ___ ___ __ ___________ __
  380. // / | \ ____ ____ | | _\__ ___/____ _____| | __
  381. // / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ /
  382. // \ Y ( <_> | <_> ) < | | / __ \_\___ \| <
  383. // \___|_ / \____/ \____/|__|_ \ |____| (____ /____ >__|_ \
  384. // \/ \/ \/ \/ \/
  385. // HookTaskType is the type of an hook task
  386. type HookTaskType int
  387. // Types of hook tasks
  388. const (
  389. GOGS HookTaskType = iota + 1
  390. SLACK
  391. GITEA
  392. DISCORD
  393. DINGTALK
  394. TELEGRAM
  395. MSTEAMS
  396. )
  397. var hookTaskTypes = map[string]HookTaskType{
  398. "gitea": GITEA,
  399. "gogs": GOGS,
  400. "slack": SLACK,
  401. "discord": DISCORD,
  402. "dingtalk": DINGTALK,
  403. "telegram": TELEGRAM,
  404. "msteams": MSTEAMS,
  405. }
  406. // ToHookTaskType returns HookTaskType by given name.
  407. func ToHookTaskType(name string) HookTaskType {
  408. return hookTaskTypes[name]
  409. }
  410. // Name returns the name of an hook task type
  411. func (t HookTaskType) Name() string {
  412. switch t {
  413. case GITEA:
  414. return "gitea"
  415. case GOGS:
  416. return "gogs"
  417. case SLACK:
  418. return "slack"
  419. case DISCORD:
  420. return "discord"
  421. case DINGTALK:
  422. return "dingtalk"
  423. case TELEGRAM:
  424. return "telegram"
  425. case MSTEAMS:
  426. return "msteams"
  427. }
  428. return ""
  429. }
  430. // IsValidHookTaskType returns true if given name is a valid hook task type.
  431. func IsValidHookTaskType(name string) bool {
  432. _, ok := hookTaskTypes[name]
  433. return ok
  434. }
  435. // HookEventType is the type of an hook event
  436. type HookEventType string
  437. // Types of hook events
  438. const (
  439. HookEventCreate HookEventType = "create"
  440. HookEventDelete HookEventType = "delete"
  441. HookEventFork HookEventType = "fork"
  442. HookEventPush HookEventType = "push"
  443. HookEventIssues HookEventType = "issues"
  444. HookEventIssueComment HookEventType = "issue_comment"
  445. HookEventPullRequest HookEventType = "pull_request"
  446. HookEventRepository HookEventType = "repository"
  447. HookEventRelease HookEventType = "release"
  448. HookEventPullRequestApproved HookEventType = "pull_request_approved"
  449. HookEventPullRequestRejected HookEventType = "pull_request_rejected"
  450. HookEventPullRequestComment HookEventType = "pull_request_comment"
  451. )
  452. // HookRequest represents hook task request information.
  453. type HookRequest struct {
  454. Headers map[string]string `json:"headers"`
  455. }
  456. // HookResponse represents hook task response information.
  457. type HookResponse struct {
  458. Status int `json:"status"`
  459. Headers map[string]string `json:"headers"`
  460. Body string `json:"body"`
  461. }
  462. // HookTask represents a hook task.
  463. type HookTask struct {
  464. ID int64 `xorm:"pk autoincr"`
  465. RepoID int64 `xorm:"INDEX"`
  466. HookID int64
  467. UUID string
  468. Type HookTaskType
  469. URL string `xorm:"TEXT"`
  470. Signature string `xorm:"TEXT"`
  471. api.Payloader `xorm:"-"`
  472. PayloadContent string `xorm:"TEXT"`
  473. HTTPMethod string `xorm:"http_method"`
  474. ContentType HookContentType
  475. EventType HookEventType
  476. IsSSL bool
  477. IsDelivered bool
  478. Delivered int64
  479. DeliveredString string `xorm:"-"`
  480. // History info.
  481. IsSucceed bool
  482. RequestContent string `xorm:"TEXT"`
  483. RequestInfo *HookRequest `xorm:"-"`
  484. ResponseContent string `xorm:"TEXT"`
  485. ResponseInfo *HookResponse `xorm:"-"`
  486. }
  487. // BeforeUpdate will be invoked by XORM before updating a record
  488. // representing this object
  489. func (t *HookTask) BeforeUpdate() {
  490. if t.RequestInfo != nil {
  491. t.RequestContent = t.simpleMarshalJSON(t.RequestInfo)
  492. }
  493. if t.ResponseInfo != nil {
  494. t.ResponseContent = t.simpleMarshalJSON(t.ResponseInfo)
  495. }
  496. }
  497. // AfterLoad updates the webhook object upon setting a column
  498. func (t *HookTask) AfterLoad() {
  499. t.DeliveredString = time.Unix(0, t.Delivered).Format("2006-01-02 15:04:05 MST")
  500. if len(t.RequestContent) == 0 {
  501. return
  502. }
  503. t.RequestInfo = &HookRequest{}
  504. if err := json.Unmarshal([]byte(t.RequestContent), t.RequestInfo); err != nil {
  505. log.Error("Unmarshal RequestContent[%d]: %v", t.ID, err)
  506. }
  507. if len(t.ResponseContent) > 0 {
  508. t.ResponseInfo = &HookResponse{}
  509. if err := json.Unmarshal([]byte(t.ResponseContent), t.ResponseInfo); err != nil {
  510. log.Error("Unmarshal ResponseContent[%d]: %v", t.ID, err)
  511. }
  512. }
  513. }
  514. func (t *HookTask) simpleMarshalJSON(v interface{}) string {
  515. p, err := json.Marshal(v)
  516. if err != nil {
  517. log.Error("Marshal [%d]: %v", t.ID, err)
  518. }
  519. return string(p)
  520. }
  521. // HookTasks returns a list of hook tasks by given conditions.
  522. func HookTasks(hookID int64, page int) ([]*HookTask, error) {
  523. tasks := make([]*HookTask, 0, setting.Webhook.PagingNum)
  524. return tasks, x.
  525. Limit(setting.Webhook.PagingNum, (page-1)*setting.Webhook.PagingNum).
  526. Where("hook_id=?", hookID).
  527. Desc("id").
  528. Find(&tasks)
  529. }
  530. // CreateHookTask creates a new hook task,
  531. // it handles conversion from Payload to PayloadContent.
  532. func CreateHookTask(t *HookTask) error {
  533. return createHookTask(x, t)
  534. }
  535. func createHookTask(e Engine, t *HookTask) error {
  536. data, err := t.Payloader.JSONPayload()
  537. if err != nil {
  538. return err
  539. }
  540. t.UUID = gouuid.NewV4().String()
  541. t.PayloadContent = string(data)
  542. _, err = e.Insert(t)
  543. return err
  544. }
  545. // UpdateHookTask updates information of hook task.
  546. func UpdateHookTask(t *HookTask) error {
  547. _, err := x.ID(t.ID).AllCols().Update(t)
  548. return err
  549. }
  550. // FindUndeliveredHookTasks represents find the undelivered hook tasks
  551. func FindUndeliveredHookTasks() ([]*HookTask, error) {
  552. tasks := make([]*HookTask, 0, 10)
  553. if err := x.Where("is_delivered=?", false).Find(&tasks); err != nil {
  554. return nil, err
  555. }
  556. return tasks, nil
  557. }
  558. // FindRepoUndeliveredHookTasks represents find the undelivered hook tasks of one repository
  559. func FindRepoUndeliveredHookTasks(repoID int64) ([]*HookTask, error) {
  560. tasks := make([]*HookTask, 0, 5)
  561. if err := x.Where("repo_id=? AND is_delivered=?", repoID, false).Find(&tasks); err != nil {
  562. return nil, err
  563. }
  564. return tasks, nil
  565. }