mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-09 17:37:20 -06:00
fix(webhook): fire repository event on repository transfer (#40200)
Transferring a repository fired no `repository` webhook. It now sends a `transferred` action shaped like GitHub's, with the previous owner in `changes.owner.from.user` or `changes.owner.from.organization`. Like GitHub, it goes to the new owner's webhooks, plus the repository's own and system webhooks. - Chat webhooks render "Repository transferred from <owner>". - Actions workflows are unaffected, no trigger matches `repository` events. Fixes https://github.com/go-gitea/gitea/issues/33584 --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -460,6 +460,13 @@ type ChangesFromPayload struct {
|
||||
From string `json:"from"`
|
||||
}
|
||||
|
||||
type ChangesOwnerPayload struct {
|
||||
From struct {
|
||||
User *User `json:"user,omitempty"`
|
||||
Organization *User `json:"organization,omitempty"`
|
||||
} `json:"from"`
|
||||
}
|
||||
|
||||
// ChangesPayload represents the payload information of issue change
|
||||
type ChangesPayload struct {
|
||||
// Changes made to the title
|
||||
@@ -470,6 +477,8 @@ type ChangesPayload struct {
|
||||
Ref *ChangesFromPayload `json:"ref,omitempty"`
|
||||
// Changes made to the repository name
|
||||
Name *ChangesFromPayload `json:"name,omitempty"`
|
||||
// Changes made to the repository owner
|
||||
Owner *ChangesOwnerPayload `json:"owner,omitempty"`
|
||||
// Changes made to the labels added
|
||||
AddedLabels []*Label `json:"added_labels"`
|
||||
// Changes made to the labels removed
|
||||
@@ -560,6 +569,8 @@ const (
|
||||
HookRepoDeleted HookRepoAction = "deleted"
|
||||
// HookRepoRenamed renamed
|
||||
HookRepoRenamed HookRepoAction = "renamed"
|
||||
// HookRepoTransferred transferred
|
||||
HookRepoTransferred HookRepoAction = "transferred"
|
||||
)
|
||||
|
||||
// RepositoryPayload payload for repository webhooks
|
||||
|
||||
@@ -2318,7 +2318,7 @@
|
||||
"repo.settings.event_branch_delete": "Branch Deletion",
|
||||
"repo.settings.event_push_desc": "Git push to a repository.",
|
||||
"repo.settings.event_repository": "Repository",
|
||||
"repo.settings.event_repository_desc": "Repository created, deleted, or renamed.",
|
||||
"repo.settings.event_repository_desc": "Repository created, deleted, renamed, or transferred.",
|
||||
"repo.settings.event_header_issue": "Issue Events",
|
||||
"repo.settings.event_issues": "Issues",
|
||||
"repo.settings.event_issues_desc": "Issue opened, closed, reopened, edited or deleted.",
|
||||
|
||||
@@ -37,8 +37,8 @@ func (n *auditNotifier) RenameRepository(ctx context.Context, doer *user_model.U
|
||||
RecordAs(ctx, doer, audit_model.RepositoryName, repo, "previous_name", oldRepoName)
|
||||
}
|
||||
|
||||
func (n *auditNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
|
||||
RecordAs(ctx, doer, audit_model.RepositoryTransferFinish, repo, "old_owner", oldOwnerName, "new_owner", repo.OwnerName)
|
||||
func (n *auditNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwner *user_model.User) {
|
||||
RecordAs(ctx, doer, audit_model.RepositoryTransferFinish, repo, "old_owner", oldOwner.Name, "new_owner", repo.OwnerName)
|
||||
}
|
||||
|
||||
func (n *auditNotifier) RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) {
|
||||
|
||||
@@ -53,7 +53,7 @@ func TestAuditNotifier(t *testing.T) {
|
||||
audit_model.Event{Action: audit_model.RepositoryCreate, ActorID: doer.ID, Origin: audit_model.OriginUI},
|
||||
},
|
||||
{
|
||||
func() { notifier.TransferRepository(ctx, doer, repo, "previous_owner") },
|
||||
func() { notifier.TransferRepository(ctx, doer, repo, &user_model.User{Name: "previous_owner"}) },
|
||||
audit_model.Event{Action: audit_model.RepositoryTransferFinish, ActorID: doer.ID, Message: "Transferred repository owner/repo from previous_owner to owner."},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -164,7 +164,7 @@ func (a *actionNotifier) RenameRepository(ctx context.Context, doer *user_model.
|
||||
}
|
||||
}
|
||||
|
||||
func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
|
||||
func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwner *user_model.User) {
|
||||
if err := NotifyWatchers(ctx, &activities_model.Action{
|
||||
ActUserID: doer.ID,
|
||||
ActUser: doer,
|
||||
@@ -172,7 +172,7 @@ func (a *actionNotifier) TransferRepository(ctx context.Context, doer *user_mode
|
||||
RepoID: repo.ID,
|
||||
Repo: repo,
|
||||
IsPrivate: repo.IsPrivate,
|
||||
Content: path.Join(oldOwnerName, repo.Name),
|
||||
Content: path.Join(oldOwner.Name, repo.Name),
|
||||
}); err != nil {
|
||||
log.Error("NotifyWatchers: %v", err)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ type Notifier interface {
|
||||
DeleteRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository)
|
||||
ForkRepository(ctx context.Context, doer *user_model.User, oldRepo, repo *repo_model.Repository)
|
||||
RenameRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldRepoName string)
|
||||
TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string)
|
||||
TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwner *user_model.User)
|
||||
RepoPendingTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository)
|
||||
|
||||
NewIssue(ctx context.Context, issue *issues_model.Issue, mentions []*user_model.User)
|
||||
|
||||
@@ -383,11 +383,11 @@ func MigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo
|
||||
}
|
||||
|
||||
// TransferRepository notifies create repository to notifiers
|
||||
func TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
|
||||
func TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwner *user_model.User) {
|
||||
defer recoverPanic()
|
||||
|
||||
for _, notifier := range notifiers {
|
||||
notifier.TransferRepository(ctx, doer, repo, oldOwnerName)
|
||||
notifier.TransferRepository(ctx, doer, repo, oldOwner)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ func (*NullNotifier) RenameRepository(ctx context.Context, doer *user_model.User
|
||||
}
|
||||
|
||||
// TransferRepository places a place holder function
|
||||
func (*NullNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
|
||||
func (*NullNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwner *user_model.User) {
|
||||
}
|
||||
|
||||
// SyncPushCommits places a place holder function
|
||||
|
||||
@@ -56,7 +56,10 @@ func AcceptTransferOwnership(ctx context.Context, repo *repo_model.Repository, d
|
||||
return err
|
||||
}
|
||||
|
||||
oldOwnerName := repo.OwnerName
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
oldOwner := repo.Owner
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if err := repoTransfer.LoadAttributes(ctx); err != nil {
|
||||
@@ -71,9 +74,6 @@ func AcceptTransferOwnership(ctx context.Context, repo *repo_model.Repository, d
|
||||
return util.ErrPermissionDenied
|
||||
}
|
||||
|
||||
if err := repo.LoadOwner(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, team := range repoTransfer.Teams {
|
||||
if repoTransfer.Recipient.ID != team.OrgID {
|
||||
return fmt.Errorf("team %d does not belong to organization", team.ID)
|
||||
@@ -86,7 +86,7 @@ func AcceptTransferOwnership(ctx context.Context, repo *repo_model.Repository, d
|
||||
}
|
||||
releaser()
|
||||
|
||||
notify_service.TransferRepository(ctx, doer, repo, oldOwnerName)
|
||||
notify_service.TransferRepository(ctx, doer, repo, oldOwner)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -451,7 +451,7 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
|
||||
}
|
||||
|
||||
var isDirectTransfer bool
|
||||
oldOwnerName := repo.OwnerName
|
||||
oldOwner := repo.Owner
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
// Admin is always allowed to transfer || user transfer repo back to his account,
|
||||
@@ -500,7 +500,7 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use
|
||||
}
|
||||
|
||||
if isDirectTransfer {
|
||||
notify_service.TransferRepository(ctx, doer, repo, oldOwnerName)
|
||||
notify_service.TransferRepository(ctx, doer, repo, oldOwner)
|
||||
} else {
|
||||
// notify users who are able to accept / reject transfer
|
||||
notify_service.RepoPendingTransfer(ctx, doer, newOwner, repo)
|
||||
|
||||
@@ -152,8 +152,8 @@ func (dc dingtalkConvertor) Repository(p *api.RepositoryPayload) (DingtalkPayloa
|
||||
Content: title,
|
||||
},
|
||||
}, nil
|
||||
case api.HookRepoRenamed:
|
||||
title := fmt.Sprintf("[%s] Repository renamed from %s", p.Repository.FullName, getRepoRenamedFrom(p))
|
||||
case api.HookRepoRenamed, api.HookRepoTransferred:
|
||||
title := fmt.Sprintf("[%s] Repository %s from %s", p.Repository.FullName, p.Action, getRepoChangedFrom(p))
|
||||
return createDingtalkPayload(title, title, "view repository", p.Repository.HTMLURL), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -241,8 +241,8 @@ func (d discordConvertor) Repository(p *api.RepositoryPayload) (DiscordPayload,
|
||||
case api.HookRepoDeleted:
|
||||
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
|
||||
color = redColor
|
||||
case api.HookRepoRenamed:
|
||||
title = fmt.Sprintf("[%s] Repository renamed from %s", p.Repository.FullName, getRepoRenamedFrom(p))
|
||||
case api.HookRepoRenamed, api.HookRepoTransferred:
|
||||
title = fmt.Sprintf("[%s] Repository %s from %s", p.Repository.FullName, p.Action, getRepoChangedFrom(p))
|
||||
url = p.Repository.HTMLURL
|
||||
color = greenColor
|
||||
}
|
||||
|
||||
@@ -148,8 +148,8 @@ func (fc feishuConvertor) Repository(p *api.RepositoryPayload) (FeishuPayload, e
|
||||
case api.HookRepoDeleted:
|
||||
text = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
|
||||
return newFeishuTextPayload(text), nil
|
||||
case api.HookRepoRenamed:
|
||||
text = fmt.Sprintf("[%s] Repository renamed from %s", p.Repository.FullName, getRepoRenamedFrom(p))
|
||||
case api.HookRepoRenamed, api.HookRepoTransferred:
|
||||
text = fmt.Sprintf("[%s] Repository %s from %s", p.Repository.FullName, p.Action, getRepoChangedFrom(p))
|
||||
return newFeishuTextPayload(text), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -328,7 +328,13 @@ func getIssueCommentPayloadInfo(p *api.IssueCommentPayload, linkFormatter linkFo
|
||||
return text, issueTitle, color
|
||||
}
|
||||
|
||||
func getRepoRenamedFrom(p *api.RepositoryPayload) string {
|
||||
func getRepoChangedFrom(p *api.RepositoryPayload) string {
|
||||
if p.Action == api.HookRepoTransferred {
|
||||
if org := p.Changes.Owner.From.Organization; org != nil {
|
||||
return org.UserName
|
||||
}
|
||||
return p.Changes.Owner.From.User.UserName
|
||||
}
|
||||
if p.Changes == nil || p.Changes.Name == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -227,8 +227,8 @@ func (m matrixConvertor) Repository(p *api.RepositoryPayload) (MatrixPayload, er
|
||||
text = fmt.Sprintf("[%s] Repository created by %s", repoLink, senderLink)
|
||||
case api.HookRepoDeleted:
|
||||
text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
|
||||
case api.HookRepoRenamed:
|
||||
text = fmt.Sprintf("[%s] Repository renamed from %s by %s", repoLink, getRepoRenamedFrom(p), senderLink)
|
||||
case api.HookRepoRenamed, api.HookRepoTransferred:
|
||||
text = fmt.Sprintf("[%s] Repository %s from %s by %s", repoLink, p.Action, getRepoChangedFrom(p), senderLink)
|
||||
}
|
||||
return m.newPayload(text)
|
||||
}
|
||||
|
||||
@@ -259,8 +259,8 @@ func (m msteamsConvertor) Repository(p *api.RepositoryPayload) (MSTeamsPayload,
|
||||
case api.HookRepoDeleted:
|
||||
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
|
||||
color = yellowColor
|
||||
case api.HookRepoRenamed:
|
||||
title = fmt.Sprintf("[%s] Repository renamed from %s", p.Repository.FullName, getRepoRenamedFrom(p))
|
||||
case api.HookRepoRenamed, api.HookRepoTransferred:
|
||||
title = fmt.Sprintf("[%s] Repository %s from %s", p.Repository.FullName, p.Action, getRepoChangedFrom(p))
|
||||
url = p.Repository.HTMLURL
|
||||
color = greenColor
|
||||
}
|
||||
|
||||
@@ -146,6 +146,24 @@ func (m *webhookNotifier) RenameRepository(ctx context.Context, doer *user_model
|
||||
}
|
||||
}
|
||||
|
||||
func (m *webhookNotifier) TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwner *user_model.User) {
|
||||
changes := &api.ChangesPayload{Owner: &api.ChangesOwnerPayload{}}
|
||||
if oldOwner.IsOrganization() {
|
||||
changes.Owner.From.Organization = convert.ToUser(ctx, oldOwner, nil)
|
||||
} else {
|
||||
changes.Owner.From.User = convert.ToUser(ctx, oldOwner, nil)
|
||||
}
|
||||
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{
|
||||
Action: api.HookRepoTransferred,
|
||||
Repository: convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeOwner}),
|
||||
Organization: convert.ToUser(ctx, repo.MustOwner(ctx), nil),
|
||||
Sender: convert.ToUser(ctx, doer, nil),
|
||||
Changes: changes,
|
||||
}); err != nil {
|
||||
log.Error("PrepareWebhooks [repo_id: %d]: %v", repo.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *webhookNotifier) MigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
||||
// Add to hook queue for created repo after session commit.
|
||||
if err := PrepareWebhooks(ctx, EventSource{Repository: repo}, webhook_module.HookEventRepository, &api.RepositoryPayload{
|
||||
|
||||
@@ -302,8 +302,8 @@ func (s slackConvertor) Repository(p *api.RepositoryPayload) (SlackPayload, erro
|
||||
text = fmt.Sprintf("[%s] Repository created by %s", repoLink, senderLink)
|
||||
case api.HookRepoDeleted:
|
||||
text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
|
||||
case api.HookRepoRenamed:
|
||||
text = fmt.Sprintf("[%s] Repository renamed from %s by %s", repoLink, getRepoRenamedFrom(p), senderLink)
|
||||
case api.HookRepoRenamed, api.HookRepoTransferred:
|
||||
text = fmt.Sprintf("[%s] Repository %s from %s by %s", repoLink, p.Action, getRepoChangedFrom(p), senderLink)
|
||||
}
|
||||
|
||||
return s.createPayload(text, nil), nil
|
||||
|
||||
@@ -115,6 +115,13 @@ func TestSlackPayload(t *testing.T) {
|
||||
pl, err = sc.Repository(p)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Repository renamed from old-repo by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
|
||||
p.Action = api.HookRepoTransferred
|
||||
p.Changes = &api.ChangesPayload{Owner: &api.ChangesOwnerPayload{}}
|
||||
p.Changes.Owner.From.Organization = &api.User{UserName: "old-org"}
|
||||
pl, err = sc.Repository(p)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "[<http://localhost:3000/test/repo|test/repo>] Repository transferred from old-org by <https://try.gitea.io/user1|user1>", pl.Text)
|
||||
})
|
||||
|
||||
t.Run("Wiki", func(t *testing.T) {
|
||||
|
||||
@@ -154,8 +154,8 @@ func (t telegramConvertor) Repository(p *api.RepositoryPayload) (TelegramPayload
|
||||
case api.HookRepoDeleted:
|
||||
title = fmt.Sprintf("[%s] Repository deleted", html.EscapeString(p.Repository.FullName))
|
||||
return createTelegramPayloadHTML(title), nil
|
||||
case api.HookRepoRenamed:
|
||||
title = fmt.Sprintf("[%s] Repository renamed from %s", htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName), html.EscapeString(getRepoRenamedFrom(p)))
|
||||
case api.HookRepoRenamed, api.HookRepoTransferred:
|
||||
title = fmt.Sprintf("[%s] Repository %s from %s", htmlLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName), p.Action, html.EscapeString(getRepoChangedFrom(p)))
|
||||
return createTelegramPayloadHTML(title), nil
|
||||
}
|
||||
return TelegramPayload{}, nil
|
||||
|
||||
@@ -150,8 +150,8 @@ func (wc wechatworkConvertor) Repository(p *api.RepositoryPayload) (WechatworkPa
|
||||
case api.HookRepoDeleted:
|
||||
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
|
||||
return newWechatworkMarkdownPayload(title), nil
|
||||
case api.HookRepoRenamed:
|
||||
title = fmt.Sprintf("[%s] Repository renamed from %s", p.Repository.FullName, getRepoRenamedFrom(p))
|
||||
case api.HookRepoRenamed, api.HookRepoTransferred:
|
||||
title = fmt.Sprintf("[%s] Repository %s from %s", p.Repository.FullName, p.Action, getRepoChangedFrom(p))
|
||||
return newWechatworkMarkdownPayload(title), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -821,6 +821,14 @@ func Test_WebhookRepository(t *testing.T) {
|
||||
assert.Equal(t, "org3/"+newName, payloads[1].Repository.FullName)
|
||||
require.NotNil(t, payloads[1].Changes.Name)
|
||||
assert.Equal(t, "repo_new", payloads[1].Changes.Name.From)
|
||||
|
||||
req = NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/transfer", &api.TransferRepoOption{NewOwner: "org3"}).
|
||||
AddTokenAuth(getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository))
|
||||
MakeRequest(t, req, http.StatusAccepted)
|
||||
|
||||
require.Len(t, payloads, 3)
|
||||
assert.Equal(t, api.HookRepoTransferred, payloads[2].Action)
|
||||
assert.Equal(t, "user2", payloads[2].Changes.Owner.From.User.UserName)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user