diff --git a/models/repo.go b/models/repo.go index 81c9d4a11fc..a629b7311f8 100644 --- a/models/repo.go +++ b/models/repo.go @@ -835,8 +835,8 @@ func wikiRemoteURL(remote string) string { } // MigrateRepository migrates a existing repository from other project hosting. -func MigrateRepository(u *User, opts MigrateRepoOptions) (*Repository, error) { - repo, err := CreateRepository(u, CreateRepoOptions{ +func MigrateRepository(doer, u *User, opts MigrateRepoOptions) (*Repository, error) { + repo, err := CreateRepository(doer, u, CreateRepoOptions{ Name: opts.Name, Description: opts.Description, IsPrivate: opts.IsPrivate, @@ -1202,7 +1202,7 @@ func IsUsableRepoName(name string) error { return isUsableName(reservedRepoNames, reservedRepoPatterns, name) } -func createRepository(e *xorm.Session, u *User, repo *Repository) (err error) { +func createRepository(e *xorm.Session, doer, u *User, repo *Repository) (err error) { if err = IsUsableRepoName(repo.Name); err != nil { return err } @@ -1249,7 +1249,15 @@ func createRepository(e *xorm.Session, u *User, repo *Repository) (err error) { return fmt.Errorf("getOwnerTeam: %v", err) } else if err = t.addRepository(e, repo); err != nil { return fmt.Errorf("addRepository: %v", err) + } else if err = prepareWebhooks(e, repo, HookEventRepository, &api.RepositoryPayload{ + Action: api.HookRepoCreated, + Repository: repo.APIFormat(AccessModeOwner), + Organization: u.APIFormat(), + Sender: doer.APIFormat(), + }); err != nil { + return fmt.Errorf("prepareWebhooks: %v", err) } + go HookQueue.Add(repo.ID) } else { // Organization automatically called this in addRepository method. if err = repo.recalculateAccesses(e); err != nil { @@ -1266,8 +1274,8 @@ func createRepository(e *xorm.Session, u *User, repo *Repository) (err error) { return nil } -// CreateRepository creates a repository for given user or organization. -func CreateRepository(u *User, opts CreateRepoOptions) (_ *Repository, err error) { +// CreateRepository creates a repository for the user/organization u. +func CreateRepository(doer, u *User, opts CreateRepoOptions) (_ *Repository, err error) { if !u.CanCreateRepo() { return nil, ErrReachLimitOfRepo{u.MaxRepoCreation} } @@ -1287,7 +1295,7 @@ func CreateRepository(u *User, opts CreateRepoOptions) (_ *Repository, err error return nil, err } - if err = createRepository(sess, u, repo); err != nil { + if err = createRepository(sess, doer, u, repo); err != nil { return nil, err } @@ -1623,7 +1631,7 @@ func UpdateRepositoryUnits(repo *Repository, units []RepoUnit) (err error) { } // DeleteRepository deletes a repository for a user or organization. -func DeleteRepository(uid, repoID int64) error { +func DeleteRepository(doer *User, uid, repoID int64) error { // In case is a organization. org, err := GetUserByID(uid) if err != nil { @@ -1781,6 +1789,18 @@ func DeleteRepository(uid, repoID int64) error { return fmt.Errorf("Commit: %v", err) } + if org.IsOrganization() { + if err = PrepareWebhooks(repo, HookEventRepository, &api.RepositoryPayload{ + Action: api.HookRepoDeleted, + Repository: repo.APIFormat(AccessModeOwner), + Organization: org.APIFormat(), + Sender: doer.APIFormat(), + }); err != nil { + return err + } + go HookQueue.Add(repo.ID) + } + return nil } @@ -1974,7 +1994,7 @@ func gatherMissingRepoRecords() ([]*Repository, error) { } // DeleteMissingRepositories deletes all repository records that lost Git files. -func DeleteMissingRepositories() error { +func DeleteMissingRepositories(doer *User) error { repos, err := gatherMissingRepoRecords() if err != nil { return fmt.Errorf("gatherMissingRepoRecords: %v", err) @@ -1986,7 +2006,7 @@ func DeleteMissingRepositories() error { for _, repo := range repos { log.Trace("Deleting %d/%d...", repo.OwnerID, repo.ID) - if err := DeleteRepository(repo.OwnerID, repo.ID); err != nil { + if err := DeleteRepository(doer, repo.OwnerID, repo.ID); err != nil { if err2 := CreateRepositoryNotice(fmt.Sprintf("DeleteRepository [%d]: %v", repo.ID, err)); err2 != nil { return fmt.Errorf("CreateRepositoryNotice: %v", err) } @@ -2226,7 +2246,7 @@ func HasForkedRepo(ownerID, repoID int64) (*Repository, bool) { } // ForkRepository forks a repository -func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Repository, err error) { +func ForkRepository(doer, u *User, oldRepo *Repository, name, desc string) (_ *Repository, err error) { forkedRepo, err := oldRepo.GetUserFork(u.ID) if err != nil { return nil, err @@ -2256,7 +2276,7 @@ func ForkRepository(u *User, oldRepo *Repository, name, desc string) (_ *Reposit return nil, err } - if err = createRepository(sess, u, repo); err != nil { + if err = createRepository(sess, doer, u, repo); err != nil { return nil, err } diff --git a/models/repo_test.go b/models/repo_test.go index 4b6fd440aaa..c1eb4e2a79f 100644 --- a/models/repo_test.go +++ b/models/repo_test.go @@ -118,13 +118,12 @@ func TestGetUserFork(t *testing.T) { func TestForkRepository(t *testing.T) { assert.NoError(t, PrepareTestDatabase()) - // User13 has repo 11 forked from repo10 - repo, err := GetRepositoryByID(10) - assert.NoError(t, err) - assert.NotNil(t, repo) + // user 13 has already forked repo10 + user := AssertExistsAndLoadBean(t, &User{ID: 13}).(*User) + repo := AssertExistsAndLoadBean(t, &Repository{ID: 10}).(*Repository) - repo, err = ForkRepository(&User{ID: 13}, repo, "test", "test") - assert.Nil(t, repo) + fork, err := ForkRepository(user, user, repo, "test", "test") + assert.Nil(t, fork) assert.Error(t, err) assert.True(t, IsErrRepoAlreadyExist(err)) } diff --git a/models/webhook.go b/models/webhook.go index 9e5742cb52f..fa0c498f33c 100644 --- a/models/webhook.go +++ b/models/webhook.go @@ -68,6 +68,7 @@ type HookEvents struct { Create bool `json:"create"` Push bool `json:"push"` PullRequest bool `json:"pull_request"` + Repository bool `json:"repository"` } // HookEvent represents events that will delivery hook. @@ -188,6 +189,12 @@ func (w *Webhook) HasPullRequestEvent() bool { (w.ChooseEvents && w.HookEvents.PullRequest) } +// HasRepositoryEvent returns if hook enabled repository event. +func (w *Webhook) HasRepositoryEvent() bool { + return w.SendEverything || + (w.ChooseEvents && w.HookEvents.Repository) +} + // EventsArray returns an array of hook events func (w *Webhook) EventsArray() []string { events := make([]string, 0, 3) @@ -246,8 +253,12 @@ func GetWebhookByOrgID(orgID, id int64) (*Webhook, error) { // GetActiveWebhooksByRepoID returns all active webhooks of repository. func GetActiveWebhooksByRepoID(repoID int64) ([]*Webhook, error) { + return getActiveWebhooksByRepoID(x, repoID) +} + +func getActiveWebhooksByRepoID(e Engine, repoID int64) ([]*Webhook, error) { webhooks := make([]*Webhook, 0, 5) - return webhooks, x.Where("is_active=?", true). + return webhooks, e.Where("is_active=?", true). Find(&webhooks, &Webhook{RepoID: repoID}) } @@ -259,7 +270,11 @@ func GetWebhooksByRepoID(repoID int64) ([]*Webhook, error) { // GetActiveWebhooksByOrgID returns all active webhooks for an organization. func GetActiveWebhooksByOrgID(orgID int64) (ws []*Webhook, err error) { - err = x. + return getActiveWebhooksByOrgID(x, orgID) +} + +func getActiveWebhooksByOrgID(e Engine, orgID int64) (ws []*Webhook, err error) { + err = e. Where("org_id=?", orgID). And("is_active=?", true). Find(&ws) @@ -379,6 +394,7 @@ const ( HookEventCreate HookEventType = "create" HookEventPush HookEventType = "push" HookEventPullRequest HookEventType = "pull_request" + HookEventRepository HookEventType = "repository" ) // HookRequest represents hook task request information. @@ -479,13 +495,17 @@ func HookTasks(hookID int64, page int) ([]*HookTask, error) { // CreateHookTask creates a new hook task, // it handles conversion from Payload to PayloadContent. func CreateHookTask(t *HookTask) error { + return createHookTask(x, t) +} + +func createHookTask(e Engine, t *HookTask) error { data, err := t.Payloader.JSONPayload() if err != nil { return err } t.UUID = gouuid.NewV4().String() t.PayloadContent = string(data) - _, err = x.Insert(t) + _, err = e.Insert(t) return err } @@ -497,6 +517,10 @@ func UpdateHookTask(t *HookTask) error { // 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) +} + +func prepareWebhook(e Engine, w *Webhook, repo *Repository, event HookEventType, p api.Payloader) error { switch event { case HookEventCreate: if !w.HasCreateEvent() { @@ -510,6 +534,10 @@ func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Pay if !w.HasPullRequestEvent() { return nil } + case HookEventRepository: + if !w.HasRepositoryEvent() { + return nil + } } var payloader api.Payloader @@ -531,7 +559,7 @@ func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Pay payloader = p } - if err = CreateHookTask(&HookTask{ + if err = createHookTask(e, &HookTask{ RepoID: repo.ID, HookID: w.ID, Type: w.HookTaskType, @@ -548,15 +576,19 @@ func PrepareWebhook(w *Webhook, repo *Repository, event HookEventType, p api.Pay // PrepareWebhooks adds new webhooks to task queue for given payload. func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) error { - ws, err := GetActiveWebhooksByRepoID(repo.ID) + 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().IsOrganization() { + if repo.mustOwner(e).IsOrganization() { // get hooks for org - orgHooks, err := GetActiveWebhooksByOrgID(repo.OwnerID) + orgHooks, err := getActiveWebhooksByOrgID(e, repo.OwnerID) if err != nil { return fmt.Errorf("GetActiveWebhooksByOrgID: %v", err) } @@ -568,7 +600,7 @@ func PrepareWebhooks(repo *Repository, event HookEventType, p api.Payloader) err } for _, w := range ws { - if err = PrepareWebhook(w, repo, event, p); err != nil { + if err = prepareWebhook(e, w, repo, event, p); err != nil { return err } } diff --git a/models/webhook_discord.go b/models/webhook_discord.go index 4426a945b4c..631c8174e97 100644 --- a/models/webhook_discord.go +++ b/models/webhook_discord.go @@ -228,6 +228,37 @@ func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) }, nil } +func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (*DiscordPayload, error) { + var title, url string + var color int + switch p.Action { + case api.HookRepoCreated: + title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName) + url = p.Repository.HTMLURL + color = successColor + case api.HookRepoDeleted: + title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName) + color = warnColor + } + + return &DiscordPayload{ + Username: meta.Username, + AvatarURL: meta.IconURL, + Embeds: []DiscordEmbed{ + { + Title: title, + URL: url, + Color: color, + Author: DiscordEmbedAuthor{ + Name: p.Sender.UserName, + URL: setting.AppURL + p.Sender.UserName, + IconURL: p.Sender.AvatarURL, + }, + }, + }, + }, nil +} + // GetDiscordPayload converts a discord webhook into a DiscordPayload func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*DiscordPayload, error) { s := new(DiscordPayload) @@ -244,6 +275,8 @@ func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*Disc return getDiscordPushPayload(p.(*api.PushPayload), discord) case HookEventPullRequest: return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord) + case HookEventRepository: + return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord) } return s, nil diff --git a/models/webhook_slack.go b/models/webhook_slack.go index b297fefd668..ca43cfd4274 100644 --- a/models/webhook_slack.go +++ b/models/webhook_slack.go @@ -189,6 +189,30 @@ func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*S }, nil } +func getSlackRepositoryPayload(p *api.RepositoryPayload, slack *SlackMeta) (*SlackPayload, error) { + senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName) + var text, title, attachmentText string + switch p.Action { + case api.HookRepoCreated: + text = fmt.Sprintf("[%s] Repository created by %s", p.Repository.FullName, senderLink) + title = p.Repository.HTMLURL + case api.HookRepoDeleted: + text = fmt.Sprintf("[%s] Repository deleted by %s", p.Repository.FullName, senderLink) + } + + return &SlackPayload{ + Channel: slack.Channel, + Text: text, + Username: slack.Username, + IconURL: slack.IconURL, + Attachments: []SlackAttachment{{ + Color: slack.Color, + Title: title, + Text: attachmentText, + }}, + }, nil +} + // GetSlackPayload converts a slack webhook into a SlackPayload func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (*SlackPayload, error) { s := new(SlackPayload) @@ -205,6 +229,8 @@ func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (*SlackP return getSlackPushPayload(p.(*api.PushPayload), slack) case HookEventPullRequest: return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack) + case HookEventRepository: + return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack) } return s, nil diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 70c9c8b4af9..2071cccb414 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -124,6 +124,7 @@ type WebhookForm struct { Create bool Push bool PullRequest bool + Repository bool Active bool } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index fbd96b3b00a..21e886b5bfa 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -892,6 +892,8 @@ settings.event_pull_request = Pull Request settings.event_pull_request_desc = Pull request opened, closed, reopened, edited, assigned, unassigned, label updated, label cleared, or synchronized. settings.event_push = Push settings.event_push_desc = Git push to a repository +settings.event_repository = Repository +settings.event_repository_desc = Repository created or deleted settings.active = Active settings.active_helper = Information about the event which triggered the hook will be sent as well. settings.add_hook_success = New webhook has been added. diff --git a/routers/admin/admin.go b/routers/admin/admin.go index d439544c8b2..94b88a05c3e 100644 --- a/routers/admin/admin.go +++ b/routers/admin/admin.go @@ -145,7 +145,7 @@ func Dashboard(ctx *context.Context) { err = models.DeleteRepositoryArchives() case cleanMissingRepos: success = ctx.Tr("admin.dashboard.delete_missing_repos_success") - err = models.DeleteMissingRepositories() + err = models.DeleteMissingRepositories(ctx.User) case gitGCRepos: success = ctx.Tr("admin.dashboard.git_gc_repos_success") err = models.GitGcRepos() diff --git a/routers/admin/repos.go b/routers/admin/repos.go index 7f885b34c96..12df6fba1e8 100644 --- a/routers/admin/repos.go +++ b/routers/admin/repos.go @@ -39,7 +39,7 @@ func DeleteRepo(ctx *context.Context) { return } - if err := models.DeleteRepository(repo.MustOwner().ID, repo.ID); err != nil { + if err := models.DeleteRepository(ctx.User, repo.MustOwner().ID, repo.ID); err != nil { ctx.Handle(500, "DeleteRepository", err) return } diff --git a/routers/api/v1/repo/fork.go b/routers/api/v1/repo/fork.go index e4f45004de5..2f3addf6922 100644 --- a/routers/api/v1/repo/fork.go +++ b/routers/api/v1/repo/fork.go @@ -73,7 +73,7 @@ func CreateFork(ctx *context.APIContext, form api.CreateForkOption) { } forker = org } - fork, err := models.ForkRepository(forker, repo, repo.Name, repo.Description) + fork, err := models.ForkRepository(ctx.User, forker, repo, repo.Name, repo.Description) if err != nil { ctx.Error(500, "ForkRepository", err) return diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 305daa064df..20393102fc8 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -105,7 +105,7 @@ func Search(ctx *context.APIContext) { // CreateUserRepo create a repository for a user func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateRepoOption) { - repo, err := models.CreateRepository(owner, models.CreateRepoOptions{ + repo, err := models.CreateRepository(ctx.User, owner, models.CreateRepoOptions{ Name: opt.Name, Description: opt.Description, Gitignores: opt.Gitignores, @@ -121,7 +121,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateR ctx.Error(422, "", err) } else { if repo != nil { - if err = models.DeleteRepository(ctx.User.ID, repo.ID); err != nil { + if err = models.DeleteRepository(ctx.User, ctx.User.ID, repo.ID); err != nil { log.Error(4, "DeleteRepository: %v", err) } } @@ -254,7 +254,7 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) { return } - repo, err := models.MigrateRepository(ctxUser, models.MigrateRepoOptions{ + repo, err := models.MigrateRepository(ctx.User, ctxUser, models.MigrateRepoOptions{ Name: form.RepoName, Description: form.Description, IsPrivate: form.Private || setting.Repository.ForcePrivate, @@ -263,7 +263,7 @@ func Migrate(ctx *context.APIContext, form auth.MigrateRepoForm) { }) if err != nil { if repo != nil { - if errDelete := models.DeleteRepository(ctxUser.ID, repo.ID); errDelete != nil { + if errDelete := models.DeleteRepository(ctx.User, ctxUser.ID, repo.ID); errDelete != nil { log.Error(4, "DeleteRepository: %v", errDelete) } } @@ -345,7 +345,7 @@ func Delete(ctx *context.APIContext) { return } - if err := models.DeleteRepository(owner.ID, repo.ID); err != nil { + if err := models.DeleteRepository(ctx.User, owner.ID, repo.ID); err != nil { ctx.Error(500, "DeleteRepository", err) return } diff --git a/routers/repo/pull.go b/routers/repo/pull.go index ad4a01ec946..47fcff3128a 100644 --- a/routers/repo/pull.go +++ b/routers/repo/pull.go @@ -125,7 +125,7 @@ func ForkPost(ctx *context.Context, form auth.CreateRepoForm) { } } - repo, err := models.ForkRepository(ctxUser, forkRepo, form.RepoName, form.Description) + repo, err := models.ForkRepository(ctx.User, ctxUser, forkRepo, form.RepoName, form.Description) if err != nil { ctx.Data["Err_RepoName"] = true switch { diff --git a/routers/repo/repo.go b/routers/repo/repo.go index 5fcbb84b9a6..ebbce1e19cd 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -127,7 +127,7 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { return } - repo, err := models.CreateRepository(ctxUser, models.CreateRepoOptions{ + repo, err := models.CreateRepository(ctx.User, ctxUser, models.CreateRepoOptions{ Name: form.RepoName, Description: form.Description, Gitignores: form.Gitignores, @@ -143,7 +143,7 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { } if repo != nil { - if errDelete := models.DeleteRepository(ctxUser.ID, repo.ID); errDelete != nil { + if errDelete := models.DeleteRepository(ctx.User, ctxUser.ID, repo.ID); errDelete != nil { log.Error(4, "DeleteRepository: %v", errDelete) } } @@ -204,7 +204,7 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { return } - repo, err := models.MigrateRepository(ctxUser, models.MigrateRepoOptions{ + repo, err := models.MigrateRepository(ctx.User, ctxUser, models.MigrateRepoOptions{ Name: form.RepoName, Description: form.Description, IsPrivate: form.Private || setting.Repository.ForcePrivate, @@ -218,7 +218,7 @@ func MigratePost(ctx *context.Context, form auth.MigrateRepoForm) { } if repo != nil { - if errDelete := models.DeleteRepository(ctxUser.ID, repo.ID); errDelete != nil { + if errDelete := models.DeleteRepository(ctx.User, ctxUser.ID, repo.ID); errDelete != nil { log.Error(4, "DeleteRepository: %v", errDelete) } } diff --git a/routers/repo/setting.go b/routers/repo/setting.go index a50c6a6e227..8b38e80916f 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -314,7 +314,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { } } - if err := models.DeleteRepository(ctx.Repo.Owner.ID, repo.ID); err != nil { + if err := models.DeleteRepository(ctx.User, ctx.Repo.Owner.ID, repo.ID); err != nil { ctx.Handle(500, "DeleteRepository", err) return } diff --git a/routers/repo/webhook.go b/routers/repo/webhook.go index 4011b6f0e6d..9c88b4385ef 100644 --- a/routers/repo/webhook.go +++ b/routers/repo/webhook.go @@ -121,6 +121,7 @@ func ParseHookEvent(form auth.WebhookForm) *models.HookEvent { Create: form.Create, Push: form.Push, PullRequest: form.PullRequest, + Repository: form.Repository, }, } } diff --git a/templates/repo/settings/hook_settings.tmpl b/templates/repo/settings/hook_settings.tmpl index e0c687dbaed..7f3406588fa 100644 --- a/templates/repo/settings/hook_settings.tmpl +++ b/templates/repo/settings/hook_settings.tmpl @@ -52,6 +52,16 @@ + +
+
+
+ + + {{.i18n.Tr "repo.settings.event_repository_desc"}} +
+
+