Skip to content

Commit b689bb6

Browse files
ethantkoeniglafriks
authored andcommitted
Webhooks for repo creation/deletion (#1663)
* Webhooks for repo creation/deletion * add createHookTask * Add handles for GetSlackPayload and GetDiscordPayload
1 parent 79f7839 commit b689bb6

File tree

16 files changed

+163
-39
lines changed

16 files changed

+163
-39
lines changed

models/repo.go

+31-11
Original file line numberDiff line numberDiff line change
@@ -835,8 +835,8 @@ func wikiRemoteURL(remote string) string {
835835
}
836836

837837
// MigrateRepository migrates a existing repository from other project hosting.
838-
func MigrateRepository(u *User, opts MigrateRepoOptions) (*Repository, error) {
839-
repo, err := CreateRepository(u, CreateRepoOptions{
838+
func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, error) {
839+
repo, err := CreateRepository(doer, u, CreateRepoOptions{
840840
Name: opts.Name,
841841
Description: opts.Description,
842842
IsPrivate: opts.IsPrivate,
@@ -1202,7 +1202,7 @@ func IsUsableRepoName(name string) error {
12021202
return isUsableName(reservedRepoNames, reservedRepoPatterns, name)
12031203
}
12041204

1205-
func createRepository(e *xorm.Session, u *User, repo *Repository) (err error) {
1205+
func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err error) {
12061206
if err = IsUsableRepoName(repo.Name); err != nil {
12071207
return err
12081208
}
@@ -1249,7 +1249,15 @@ func createRepository(e *xorm.Session, u *User, repo *Repository) (err error) {
12491249
return fmt.Errorf("getOwnerTeam: %v", err)
12501250
} else if err = t.addRepository(e, repo); err != nil {
12511251
return fmt.Errorf("addRepository: %v", err)
1252+
} else if err = prepareWebhooks(e, repo, HookEventRepository, &api.RepositoryPayload{
1253+
Action: api.HookRepoCreated,
1254+
Repository: repo.APIFormat(AccessModeOwner),
1255+
Organization: u.APIFormat(),
1256+
Sender: doer.APIFormat(),
1257+
}); err != nil {
1258+
return fmt.Errorf("prepareWebhooks: %v", err)
12521259
}
1260+
go HookQueue.Add(repo.ID)
12531261
} else {
12541262
// Organization automatically called this in addRepository method.
12551263
if err = repo.recalculateAccesses(e); err != nil {
@@ -1266,8 +1274,8 @@ func createRepository(e *xorm.Session, u *User, repo *Repository) (err error) {
12661274
return nil
12671275
}
12681276

1269-
// CreateRepository creates a repository for given user or organization.
1270-
func CreateRepository(u *User, opts CreateRepoOptions) (_ *Repository, err error) {
1277+
// CreateRepository creates a repository for the user/organization u.
1278+
func CreateRepository(doer, u *User, opts CreateRepoOptions) (_ *Repository, err error) {
12711279
if !u.CanCreateRepo() {
12721280
return nil, ErrReachLimitOfRepo{u.MaxRepoCreation}
12731281
}
@@ -1287,7 +1295,7 @@ func CreateRepository(u *User, opts CreateRepoOptions) (_ *Repository, err error
12871295
return nil, err
12881296
}
12891297

1290-
if err = createRepository(sess, u, repo); err != nil {
1298+
if err = createRepository(sess, doer, u, repo); err != nil {
12911299
return nil, err
12921300
}
12931301

@@ -1623,7 +1631,7 @@ func UpdateRepositoryUnits(repo *Repository, units []RepoUnit) (err error) {
16231631
}
16241632

16251633
// DeleteRepository deletes a repository for a user or organization.
1626-
func DeleteRepository(uid, repoID int64) error {
1634+
func DeleteRepository(doer *User, uid, repoID int64) error {
16271635
// In case is a organization.
16281636
org, err := GetUserByID(uid)
16291637
if err != nil {
@@ -1781,6 +1789,18 @@ func DeleteRepository(uid, repoID int64) error {
17811789
return fmt.Errorf("Commit: %v", err)
17821790
}
17831791

1792+
if org.IsOrganization() {
1793+
if err = PrepareWebhooks(repo, HookEventRepository, &api.RepositoryPayload{
1794+
Action: api.HookRepoDeleted,
1795+
Repository: repo.APIFormat(AccessModeOwner),
1796+
Organization: org.APIFormat(),
1797+
Sender: doer.APIFormat(),
1798+
}); err != nil {
1799+
return err
1800+
}
1801+
go HookQueue.Add(repo.ID)
1802+
}
1803+
17841804
return nil
17851805
}
17861806

@@ -1974,7 +1994,7 @@ func gatherMissingRepoRecords() ([]*Repository, error) {
19741994
}
19751995

19761996
// DeleteMissingRepositories deletes all repository records that lost Git files.
1977-
func DeleteMissingRepositories() error {
1997+
func DeleteMissingRepositories(doer *User) error {
19781998
repos, err := gatherMissingRepoRecords()
19791999
if err != nil {
19802000
return fmt.Errorf("gatherMissingRepoRecords: %v", err)
@@ -1986,7 +2006,7 @@ func DeleteMissingRepositories() error {
19862006

19872007
for _, repo := range repos {
19882008
log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID)
1989-
if err := DeleteRepository(repo.OwnerID, repo.ID); err != nil {
2009+
if err := DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil {
19902010
if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteRepository [%d]: %v", repo.ID, err)); err2 != nil {
19912011
return fmt.Errorf("CreateRepositoryNotice: %v", err)
19922012
}
@@ -2226,7 +2246,7 @@ func HasForkedRepo(ownerID, repoID int64) (*Repository, bool) {
22262246
}
22272247

22282248
// ForkRepository forks a repository
2229-
func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Repository, err error) {
2249+
func ForkRepository(doer, u *User, oldRepo *Repository, name, desc string) (_ *Repository, err error) {
22302250
forkedRepo, err := oldRepo.GetUserFork(u.ID)
22312251
if err != nil {
22322252
return nil, err
@@ -2256,7 +2276,7 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Reposit
22562276
return nil, err
22572277
}
22582278

2259-
if err = createRepository(sess, u, repo); err != nil {
2279+
if err = createRepository(sess, doer, u, repo); err != nil {
22602280
return nil, err
22612281
}
22622282

models/repo_test.go

+5-6
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,12 @@ func TestGetUserFork(t *testing.T) {
118118
func TestForkRepository(t *testing.T) {
119119
assert.NoError(t, PrepareTestDatabase())
120120

121-
// User13 has repo 11 forked from repo10
122-
repo, err := GetRepositoryByID(10)
123-
assert.NoError(t, err)
124-
assert.NotNil(t, repo)
121+
// user 13 has already forked repo10
122+
user := AssertExistsAndLoadBean(t, &User{ID: 13}).(*User)
123+
repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository)
125124

126-
repo, err = ForkRepository(&User{ID: 13}, repo, "test", "test")
127-
assert.Nil(t, repo)
125+
fork, err := ForkRepository(user, user, repo, "test", "test")
126+
assert.Nil(t, fork)
128127
assert.Error(t, err)
129128
assert.True(t, IsErrRepoAlreadyExist(err))
130129
}

models/webhook.go

+40-8
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ type HookEvents struct {
6868
Create bool `json:"create"`
6969
Push bool `json:"push"`
7070
PullRequest bool `json:"pull_request"`
71+
Repository bool `json:"repository"`
7172
}
7273

7374
// HookEvent represents events that will delivery hook.
@@ -188,6 +189,12 @@ func (w *Webhook) HasPullRequestEvent() bool {
188189
(w.ChooseEvents && w.HookEvents.PullRequest)
189190
}
190191

192+
// HasRepositoryEvent returns if hook enabled repository event.
193+
func (w *Webhook) HasRepositoryEvent() bool {
194+
return w.SendEverything ||
195+
(w.ChooseEvents && w.HookEvents.Repository)
196+
}
197+
191198
// EventsArray returns an array of hook events
192199
func (w *Webhook) EventsArray() []string {
193200
events := make([]string, 0, 3)
@@ -246,8 +253,12 @@ func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) {
246253

247254
// GetActiveWebhooksByRepoID returns all active webhooks of repository.
248255
func GetActiveWebhooksByRepoID(repoID int64) ([]*Webhook, error) {
256+
return getActiveWebhooksByRepoID(x, repoID)
257+
}
258+
259+
func getActiveWebhooksByRepoID(e Engine, repoID int64) ([]*Webhook, error) {
249260
webhooks := make([]*Webhook, 0, 5)
250-
return webhooks, x.Where("is_active=?", true).
261+
return webhooks, e.Where("is_active=?", true).
251262
Find(&webhooks, &Webhook{RepoID: repoID})
252263
}
253264

@@ -259,7 +270,11 @@ func GetWebhooksByRepoID(repoID int64) ([]*Webhook, error) {
259270

260271
// GetActiveWebhooksByOrgID returns all active webhooks for an organization.
261272
func GetActiveWebhooksByOrgID(orgID int64) (ws []*Webhook, err error) {
262-
err = x.
273+
return getActiveWebhooksByOrgID(x, orgID)
274+
}
275+
276+
func getActiveWebhooksByOrgID(e Engine, orgID int64) (ws []*Webhook, err error) {
277+
err = e.
263278
Where("org_id=?", orgID).
264279
And("is_active=?", true).
265280
Find(&ws)
@@ -379,6 +394,7 @@ const (
379394
HookEventCreate HookEventType = "create"
380395
HookEventPush HookEventType = "push"
381396
HookEventPullRequest HookEventType = "pull_request"
397+
HookEventRepository HookEventType = "repository"
382398
)
383399

384400
// HookRequest represents hook task request information.
@@ -479,13 +495,17 @@ func HookTasks(hookID int64, page int) ([]*HookTask, error) {
479495
// CreateHookTask creates a new hook task,
480496
// it handles conversion from Payload to PayloadContent.
481497
func CreateHookTask(t *HookTask) error {
498+
return createHookTask(x, t)
499+
}
500+
501+
func createHookTask(e Engine, t *HookTask) error {
482502
data, err := t.Payloader.JSONPayload()
483503
if err != nil {
484504
return err
485505
}
486506
t.UUID = gouuid.NewV4().String()
487507
t.PayloadContent = string(data)
488-
_, err = x.Insert(t)
508+
_, err = e.Insert(t)
489509
return err
490510
}
491511

@@ -497,6 +517,10 @@ func UpdateHookTask(t *HookTask) error {
497517

498518
// PrepareWebhook adds special webhook to task queue for given payload.
499519
func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Payloader) error {
520+
return prepareWebhook(x, w, repo, event, p)
521+
}
522+
523+
func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType, p api.Payloader) error {
500524
switch event {
501525
case HookEventCreate:
502526
if !w.HasCreateEvent() {
@@ -510,6 +534,10 @@ func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Pay
510534
if !w.HasPullRequestEvent() {
511535
return nil
512536
}
537+
case HookEventRepository:
538+
if !w.HasRepositoryEvent() {
539+
return nil
540+
}
513541
}
514542

515543
var payloader api.Payloader
@@ -531,7 +559,7 @@ func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Pay
531559
payloader = p
532560
}
533561

534-
if err = CreateHookTask(&HookTask{
562+
if err = createHookTask(e, &HookTask{
535563
RepoID: repo.ID,
536564
HookID: w.ID,
537565
Type: w.HookTaskType,
@@ -548,15 +576,19 @@ func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Pay
548576

549577
// PrepareWebhooks adds new webhooks to task queue for given payload.
550578
func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) error {
551-
ws, err := GetActiveWebhooksByRepoID(repo.ID)
579+
return prepareWebhooks(x, repo, event, p)
580+
}
581+
582+
func prepareWebhooks(e Engine, repo *Repository, event HookEventType, p api.Payloader) error {
583+
ws, err := getActiveWebhooksByRepoID(e, repo.ID)
552584
if err != nil {
553585
return fmt.Errorf("GetActiveWebhooksByRepoID: %v", err)
554586
}
555587

556588
// check if repo belongs to org and append additional webhooks
557-
if repo.MustOwner().IsOrganization() {
589+
if repo.mustOwner(e).IsOrganization() {
558590
// get hooks for org
559-
orgHooks, err := GetActiveWebhooksByOrgID(repo.OwnerID)
591+
orgHooks, err := getActiveWebhooksByOrgID(e, repo.OwnerID)
560592
if err != nil {
561593
return fmt.Errorf("GetActiveWebhooksByOrgID: %v", err)
562594
}
@@ -568,7 +600,7 @@ func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) err
568600
}
569601

570602
for _, w := range ws {
571-
if err = PrepareWebhook(w, repo, event, p); err != nil {
603+
if err = prepareWebhook(e, w, repo, event, p); err != nil {
572604
return err
573605
}
574606
}

models/webhook_discord.go

+33
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,37 @@ func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta)
228228
}, nil
229229
}
230230

231+
func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (*DiscordPayload, error) {
232+
var title, url string
233+
var color int
234+
switch p.Action {
235+
case api.HookRepoCreated:
236+
title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
237+
url = p.Repository.HTMLURL
238+
color = successColor
239+
case api.HookRepoDeleted:
240+
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
241+
color = warnColor
242+
}
243+
244+
return &DiscordPayload{
245+
Username: meta.Username,
246+
AvatarURL: meta.IconURL,
247+
Embeds: []DiscordEmbed{
248+
{
249+
Title: title,
250+
URL: url,
251+
Color: color,
252+
Author: DiscordEmbedAuthor{
253+
Name: p.Sender.UserName,
254+
URL: setting.AppURL + p.Sender.UserName,
255+
IconURL: p.Sender.AvatarURL,
256+
},
257+
},
258+
},
259+
}, nil
260+
}
261+
231262
// GetDiscordPayload converts a discord webhook into a DiscordPayload
232263
func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*DiscordPayload, error) {
233264
s := new(DiscordPayload)
@@ -244,6 +275,8 @@ func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*Disc
244275
return getDiscordPushPayload(p.(*api.PushPayload), discord)
245276
case HookEventPullRequest:
246277
return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord)
278+
case HookEventRepository:
279+
return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord)
247280
}
248281

249282
return s, nil

models/webhook_slack.go

+26
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,30 @@ func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*S
189189
}, nil
190190
}
191191

192+
func getSlackRepositoryPayload(p *api.RepositoryPayload, slack *SlackMeta) (*SlackPayload, error) {
193+
senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
194+
var text, title, attachmentText string
195+
switch p.Action {
196+
case api.HookRepoCreated:
197+
text = fmt.Sprintf("[%s] Repository created by %s", p.Repository.FullName, senderLink)
198+
title = p.Repository.HTMLURL
199+
case api.HookRepoDeleted:
200+
text = fmt.Sprintf("[%s] Repository deleted by %s", p.Repository.FullName, senderLink)
201+
}
202+
203+
return &SlackPayload{
204+
Channel: slack.Channel,
205+
Text: text,
206+
Username: slack.Username,
207+
IconURL: slack.IconURL,
208+
Attachments: []SlackAttachment{{
209+
Color: slack.Color,
210+
Title: title,
211+
Text: attachmentText,
212+
}},
213+
}, nil
214+
}
215+
192216
// GetSlackPayload converts a slack webhook into a SlackPayload
193217
func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (*SlackPayload, error) {
194218
s := new(SlackPayload)
@@ -205,6 +229,8 @@ func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (*SlackP
205229
return getSlackPushPayload(p.(*api.PushPayload), slack)
206230
case HookEventPullRequest:
207231
return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack)
232+
case HookEventRepository:
233+
return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack)
208234
}
209235

210236
return s, nil

modules/auth/repo_form.go

+1
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ type WebhookForm struct {
124124
Create bool
125125
Push bool
126126
PullRequest bool
127+
Repository bool
127128
Active bool
128129
}
129130

options/locale/locale_en-US.ini

+2
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,8 @@ settings.event_pull_request = Pull Request
892892
settings.event_pull_request_desc = Pull request opened, closed, reopened, edited, assigned, unassigned, label updated, label cleared, or synchronized.
893893
settings.event_push = Push
894894
settings.event_push_desc = Git push to a repository
895+
settings.event_repository = Repository
896+
settings.event_repository_desc = Repository created or deleted
895897
settings.active = Active
896898
settings.active_helper = Information about the event which triggered the hook will be sent as well.
897899
settings.add_hook_success = New webhook has been added.

routers/admin/admin.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ func Dashboard(ctx *context.Context) {
145145
err = models.DeleteRepositoryArchives()
146146
case cleanMissingRepos:
147147
success = ctx.Tr("admin.dashboard.delete_missing_repos_success")
148-
err = models.DeleteMissingRepositories()
148+
err = models.DeleteMissingRepositories(ctx.User)
149149
case gitGCRepos:
150150
success = ctx.Tr("admin.dashboard.git_gc_repos_success")
151151
err = models.GitGcRepos()

routers/admin/repos.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ func DeleteRepo(ctx *context.Context) {
3939
return
4040
}
4141

42-
if err := models.DeleteRepository(repo.MustOwner().ID, repo.ID); err != nil {
42+
if err := models.DeleteRepository(ctx.User, repo.MustOwner().ID, repo.ID); err != nil {
4343
ctx.Handle(500, "DeleteRepository", err)
4444
return
4545
}

0 commit comments

Comments
 (0)