aboutsummaryrefslogtreecommitdiff
path: root/internal/db/action.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/db/action.go')
-rw-r--r--internal/db/action.go767
1 files changed, 767 insertions, 0 deletions
diff --git a/internal/db/action.go b/internal/db/action.go
new file mode 100644
index 00000000..d6006410
--- /dev/null
+++ b/internal/db/action.go
@@ -0,0 +1,767 @@
+// Copyright 2014 The Gogs 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 db
+
+import (
+ "fmt"
+ "path"
+ "regexp"
+ "strings"
+ "time"
+ "unicode"
+
+ "github.com/json-iterator/go"
+ "github.com/unknwon/com"
+ log "gopkg.in/clog.v1"
+ "xorm.io/xorm"
+
+ "github.com/gogs/git-module"
+ api "github.com/gogs/go-gogs-client"
+
+ "gogs.io/gogs/internal/db/errors"
+ "gogs.io/gogs/internal/setting"
+ "gogs.io/gogs/internal/tool"
+)
+
+type ActionType int
+
+// Note: To maintain backward compatibility only append to the end of list
+const (
+ ACTION_CREATE_REPO ActionType = iota + 1 // 1
+ ACTION_RENAME_REPO // 2
+ ACTION_STAR_REPO // 3
+ ACTION_WATCH_REPO // 4
+ ACTION_COMMIT_REPO // 5
+ ACTION_CREATE_ISSUE // 6
+ ACTION_CREATE_PULL_REQUEST // 7
+ ACTION_TRANSFER_REPO // 8
+ ACTION_PUSH_TAG // 9
+ ACTION_COMMENT_ISSUE // 10
+ ACTION_MERGE_PULL_REQUEST // 11
+ ACTION_CLOSE_ISSUE // 12
+ ACTION_REOPEN_ISSUE // 13
+ ACTION_CLOSE_PULL_REQUEST // 14
+ ACTION_REOPEN_PULL_REQUEST // 15
+ ACTION_CREATE_BRANCH // 16
+ ACTION_DELETE_BRANCH // 17
+ ACTION_DELETE_TAG // 18
+ ACTION_FORK_REPO // 19
+ ACTION_MIRROR_SYNC_PUSH // 20
+ ACTION_MIRROR_SYNC_CREATE // 21
+ ACTION_MIRROR_SYNC_DELETE // 22
+)
+
+var (
+ // Same as Github. See https://help.github.com/articles/closing-issues-via-commit-messages
+ IssueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
+ IssueReopenKeywords = []string{"reopen", "reopens", "reopened"}
+
+ IssueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(IssueCloseKeywords))
+ IssueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(IssueReopenKeywords))
+ IssueReferenceKeywordsPat = regexp.MustCompile(`(?i)(?:)(^| )\S+`)
+)
+
+func assembleKeywordsPattern(words []string) string {
+ return fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(words, "|"))
+}
+
+// Action represents user operation type and other information to repository,
+// it implemented interface base.Actioner so that can be used in template render.
+type Action struct {
+ ID int64
+ UserID int64 // Receiver user ID
+ OpType ActionType
+ ActUserID int64 // Doer user ID
+ ActUserName string // Doer user name
+ ActAvatar string `xorm:"-" json:"-"`
+ RepoID int64 `xorm:"INDEX"`
+ RepoUserName string
+ RepoName string
+ RefName string
+ IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
+ Content string `xorm:"TEXT"`
+ Created time.Time `xorm:"-" json:"-"`
+ CreatedUnix int64
+}
+
+func (a *Action) BeforeInsert() {
+ a.CreatedUnix = time.Now().Unix()
+}
+
+func (a *Action) AfterSet(colName string, _ xorm.Cell) {
+ switch colName {
+ case "created_unix":
+ a.Created = time.Unix(a.CreatedUnix, 0).Local()
+ }
+}
+
+func (a *Action) GetOpType() int {
+ return int(a.OpType)
+}
+
+func (a *Action) GetActUserName() string {
+ return a.ActUserName
+}
+
+func (a *Action) ShortActUserName() string {
+ return tool.EllipsisString(a.ActUserName, 20)
+}
+
+func (a *Action) GetRepoUserName() string {
+ return a.RepoUserName
+}
+
+func (a *Action) ShortRepoUserName() string {
+ return tool.EllipsisString(a.RepoUserName, 20)
+}
+
+func (a *Action) GetRepoName() string {
+ return a.RepoName
+}
+
+func (a *Action) ShortRepoName() string {
+ return tool.EllipsisString(a.RepoName, 33)
+}
+
+func (a *Action) GetRepoPath() string {
+ return path.Join(a.RepoUserName, a.RepoName)
+}
+
+func (a *Action) ShortRepoPath() string {
+ return path.Join(a.ShortRepoUserName(), a.ShortRepoName())
+}
+
+func (a *Action) GetRepoLink() string {
+ if len(setting.AppSubURL) > 0 {
+ return path.Join(setting.AppSubURL, a.GetRepoPath())
+ }
+ return "/" + a.GetRepoPath()
+}
+
+func (a *Action) GetBranch() string {
+ return a.RefName
+}
+
+func (a *Action) GetContent() string {
+ return a.Content
+}
+
+func (a *Action) GetCreate() time.Time {
+ return a.Created
+}
+
+func (a *Action) GetIssueInfos() []string {
+ return strings.SplitN(a.Content, "|", 2)
+}
+
+func (a *Action) GetIssueTitle() string {
+ index := com.StrTo(a.GetIssueInfos()[0]).MustInt64()
+ issue, err := GetIssueByIndex(a.RepoID, index)
+ if err != nil {
+ log.Error(4, "GetIssueByIndex: %v", err)
+ return "500 when get issue"
+ }
+ return issue.Title
+}
+
+func (a *Action) GetIssueContent() string {
+ index := com.StrTo(a.GetIssueInfos()[0]).MustInt64()
+ issue, err := GetIssueByIndex(a.RepoID, index)
+ if err != nil {
+ log.Error(4, "GetIssueByIndex: %v", err)
+ return "500 when get issue"
+ }
+ return issue.Content
+}
+
+func newRepoAction(e Engine, doer, owner *User, repo *Repository) (err error) {
+ opType := ACTION_CREATE_REPO
+ if repo.IsFork {
+ opType = ACTION_FORK_REPO
+ }
+
+ return notifyWatchers(e, &Action{
+ ActUserID: doer.ID,
+ ActUserName: doer.Name,
+ OpType: opType,
+ RepoID: repo.ID,
+ RepoUserName: repo.Owner.Name,
+ RepoName: repo.Name,
+ IsPrivate: repo.IsPrivate,
+ })
+}
+
+// NewRepoAction adds new action for creating repository.
+func NewRepoAction(doer, owner *User, repo *Repository) (err error) {
+ return newRepoAction(x, doer, owner, repo)
+}
+
+func renameRepoAction(e Engine, actUser *User, oldRepoName string, repo *Repository) (err error) {
+ if err = notifyWatchers(e, &Action{
+ ActUserID: actUser.ID,
+ ActUserName: actUser.Name,
+ OpType: ACTION_RENAME_REPO,
+ RepoID: repo.ID,
+ RepoUserName: repo.Owner.Name,
+ RepoName: repo.Name,
+ IsPrivate: repo.IsPrivate,
+ Content: oldRepoName,
+ }); err != nil {
+ return fmt.Errorf("notify watchers: %v", err)
+ }
+
+ log.Trace("action.renameRepoAction: %s/%s", actUser.Name, repo.Name)
+ return nil
+}
+
+// RenameRepoAction adds new action for renaming a repository.
+func RenameRepoAction(actUser *User, oldRepoName string, repo *Repository) error {
+ return renameRepoAction(x, actUser, oldRepoName, repo)
+}
+
+func issueIndexTrimRight(c rune) bool {
+ return !unicode.IsDigit(c)
+}
+
+type PushCommit struct {
+ Sha1 string
+ Message string
+ AuthorEmail string
+ AuthorName string
+ CommitterEmail string
+ CommitterName string
+ Timestamp time.Time
+}
+
+type PushCommits struct {
+ Len int
+ Commits []*PushCommit
+ CompareURL string
+
+ avatars map[string]string
+}
+
+func NewPushCommits() *PushCommits {
+ return &PushCommits{
+ avatars: make(map[string]string),
+ }
+}
+
+func (pc *PushCommits) ToApiPayloadCommits(repoPath, repoURL string) ([]*api.PayloadCommit, error) {
+ commits := make([]*api.PayloadCommit, len(pc.Commits))
+ for i, commit := range pc.Commits {
+ authorUsername := ""
+ author, err := GetUserByEmail(commit.AuthorEmail)
+ if err == nil {
+ authorUsername = author.Name
+ } else if !errors.IsUserNotExist(err) {
+ return nil, fmt.Errorf("GetUserByEmail: %v", err)
+ }
+
+ committerUsername := ""
+ committer, err := GetUserByEmail(commit.CommitterEmail)
+ if err == nil {
+ committerUsername = committer.Name
+ } else if !errors.IsUserNotExist(err) {
+ return nil, fmt.Errorf("GetUserByEmail: %v", err)
+ }
+
+ fileStatus, err := git.GetCommitFileStatus(repoPath, commit.Sha1)
+ if err != nil {
+ return nil, fmt.Errorf("FileStatus [commit_sha1: %s]: %v", commit.Sha1, err)
+ }
+
+ commits[i] = &api.PayloadCommit{
+ ID: commit.Sha1,
+ Message: commit.Message,
+ URL: fmt.Sprintf("%s/commit/%s", repoURL, commit.Sha1),
+ Author: &api.PayloadUser{
+ Name: commit.AuthorName,
+ Email: commit.AuthorEmail,
+ UserName: authorUsername,
+ },
+ Committer: &api.PayloadUser{
+ Name: commit.CommitterName,
+ Email: commit.CommitterEmail,
+ UserName: committerUsername,
+ },
+ Added: fileStatus.Added,
+ Removed: fileStatus.Removed,
+ Modified: fileStatus.Modified,
+ Timestamp: commit.Timestamp,
+ }
+ }
+ return commits, nil
+}
+
+// AvatarLink tries to match user in database with e-mail
+// in order to show custom avatar, and falls back to general avatar link.
+func (push *PushCommits) AvatarLink(email string) string {
+ _, ok := push.avatars[email]
+ if !ok {
+ u, err := GetUserByEmail(email)
+ if err != nil {
+ push.avatars[email] = tool.AvatarLink(email)
+ if !errors.IsUserNotExist(err) {
+ log.Error(4, "GetUserByEmail: %v", err)
+ }
+ } else {
+ push.avatars[email] = u.RelAvatarLink()
+ }
+ }
+
+ return push.avatars[email]
+}
+
+// UpdateIssuesCommit checks if issues are manipulated by commit message.
+func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) error {
+ // Commits are appended in the reverse order.
+ for i := len(commits) - 1; i >= 0; i-- {
+ c := commits[i]
+
+ refMarked := make(map[int64]bool)
+ for _, ref := range IssueReferenceKeywordsPat.FindAllString(c.Message, -1) {
+ ref = ref[strings.IndexByte(ref, byte(' '))+1:]
+ ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
+
+ if len(ref) == 0 {
+ continue
+ }
+
+ // Add repo name if missing
+ if ref[0] == '#' {
+ ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
+ } else if !strings.Contains(ref, "/") {
+ // FIXME: We don't support User#ID syntax yet
+ // return ErrNotImplemented
+ continue
+ }
+
+ issue, err := GetIssueByRef(ref)
+ if err != nil {
+ if errors.IsIssueNotExist(err) {
+ continue
+ }
+ return err
+ }
+
+ if refMarked[issue.ID] {
+ continue
+ }
+ refMarked[issue.ID] = true
+
+ msgLines := strings.Split(c.Message, "\n")
+ shortMsg := msgLines[0]
+ if len(msgLines) > 2 {
+ shortMsg += "..."
+ }
+ message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, shortMsg)
+ if err = CreateRefComment(doer, repo, issue, message, c.Sha1); err != nil {
+ return err
+ }
+ }
+
+ refMarked = make(map[int64]bool)
+ // FIXME: can merge this one and next one to a common function.
+ for _, ref := range IssueCloseKeywordsPat.FindAllString(c.Message, -1) {
+ ref = ref[strings.IndexByte(ref, byte(' '))+1:]
+ ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
+
+ if len(ref) == 0 {
+ continue
+ }
+
+ // Add repo name if missing
+ if ref[0] == '#' {
+ ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
+ } else if !strings.Contains(ref, "/") {
+ // FIXME: We don't support User#ID syntax yet
+ continue
+ }
+
+ issue, err := GetIssueByRef(ref)
+ if err != nil {
+ if errors.IsIssueNotExist(err) {
+ continue
+ }
+ return err
+ }
+
+ if refMarked[issue.ID] {
+ continue
+ }
+ refMarked[issue.ID] = true
+
+ if issue.RepoID != repo.ID || issue.IsClosed {
+ continue
+ }
+
+ if err = issue.ChangeStatus(doer, repo, true); err != nil {
+ return err
+ }
+ }
+
+ // It is conflict to have close and reopen at same time, so refsMarkd doesn't need to reinit here.
+ for _, ref := range IssueReopenKeywordsPat.FindAllString(c.Message, -1) {
+ ref = ref[strings.IndexByte(ref, byte(' '))+1:]
+ ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
+
+ if len(ref) == 0 {
+ continue
+ }
+
+ // Add repo name if missing
+ if ref[0] == '#' {
+ ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
+ } else if !strings.Contains(ref, "/") {
+ // We don't support User#ID syntax yet
+ // return ErrNotImplemented
+ continue
+ }
+
+ issue, err := GetIssueByRef(ref)
+ if err != nil {
+ if errors.IsIssueNotExist(err) {
+ continue
+ }
+ return err
+ }
+
+ if refMarked[issue.ID] {
+ continue
+ }
+ refMarked[issue.ID] = true
+
+ if issue.RepoID != repo.ID || !issue.IsClosed {
+ continue
+ }
+
+ if err = issue.ChangeStatus(doer, repo, false); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+}
+
+type CommitRepoActionOptions struct {
+ PusherName string
+ RepoOwnerID int64
+ RepoName string
+ RefFullName string
+ OldCommitID string
+ NewCommitID string
+ Commits *PushCommits
+}
+
+// CommitRepoAction adds new commit actio to the repository, and prepare corresponding webhooks.
+func CommitRepoAction(opts CommitRepoActionOptions) error {
+ pusher, err := GetUserByName(opts.PusherName)
+ if err != nil {
+ return fmt.Errorf("GetUserByName [%s]: %v", opts.PusherName, err)
+ }
+
+ repo, err := GetRepositoryByName(opts.RepoOwnerID, opts.RepoName)
+ if err != nil {
+ return fmt.Errorf("GetRepositoryByName [owner_id: %d, name: %s]: %v", opts.RepoOwnerID, opts.RepoName, err)
+ }
+
+ // Change repository bare status and update last updated time.
+ repo.IsBare = false
+ if err = UpdateRepository(repo, false); err != nil {
+ return fmt.Errorf("UpdateRepository: %v", err)
+ }
+
+ isNewRef := opts.OldCommitID == git.EMPTY_SHA
+ isDelRef := opts.NewCommitID == git.EMPTY_SHA
+
+ opType := ACTION_COMMIT_REPO
+ // Check if it's tag push or branch.
+ if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) {
+ opType = ACTION_PUSH_TAG
+ } else {
+ // if not the first commit, set the compare URL.
+ if !isNewRef && !isDelRef {
+ opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
+ }
+
+ // Only update issues via commits when internal issue tracker is enabled
+ if repo.EnableIssues && !repo.EnableExternalTracker {
+ if err = UpdateIssuesCommit(pusher, repo, opts.Commits.Commits); err != nil {
+ log.Error(2, "UpdateIssuesCommit: %v", err)
+ }
+ }
+ }
+
+ if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum {
+ opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum]
+ }
+
+ data, err := jsoniter.Marshal(opts.Commits)
+ if err != nil {
+ return fmt.Errorf("Marshal: %v", err)
+ }
+
+ refName := git.RefEndName(opts.RefFullName)
+ action := &Action{
+ ActUserID: pusher.ID,
+ ActUserName: pusher.Name,
+ Content: string(data),
+ RepoID: repo.ID,
+ RepoUserName: repo.MustOwner().Name,
+ RepoName: repo.Name,
+ RefName: refName,
+ IsPrivate: repo.IsPrivate,
+ }
+
+ apiRepo := repo.APIFormat(nil)
+ apiPusher := pusher.APIFormat()
+ switch opType {
+ case ACTION_COMMIT_REPO: // Push
+ if isDelRef {
+ if err = PrepareWebhooks(repo, HOOK_EVENT_DELETE, &api.DeletePayload{
+ Ref: refName,
+ RefType: "branch",
+ PusherType: api.PUSHER_TYPE_USER,
+ Repo: apiRepo,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks.(delete branch): %v", err)
+ }
+
+ action.OpType = ACTION_DELETE_BRANCH
+ if err = NotifyWatchers(action); err != nil {
+ return fmt.Errorf("NotifyWatchers.(delete branch): %v", err)
+ }
+
+ // Delete branch doesn't have anything to push or compare
+ return nil
+ }
+
+ compareURL := setting.AppURL + opts.Commits.CompareURL
+ if isNewRef {
+ compareURL = ""
+ if err = PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
+ Ref: refName,
+ RefType: "branch",
+ DefaultBranch: repo.DefaultBranch,
+ Repo: apiRepo,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks.(new branch): %v", err)
+ }
+
+ action.OpType = ACTION_CREATE_BRANCH
+ if err = NotifyWatchers(action); err != nil {
+ return fmt.Errorf("NotifyWatchers.(new branch): %v", err)
+ }
+ }
+
+ commits, err := opts.Commits.ToApiPayloadCommits(repo.RepoPath(), repo.HTMLURL())
+ if err != nil {
+ return fmt.Errorf("ToApiPayloadCommits: %v", err)
+ }
+
+ if err = PrepareWebhooks(repo, HOOK_EVENT_PUSH, &api.PushPayload{
+ Ref: opts.RefFullName,
+ Before: opts.OldCommitID,
+ After: opts.NewCommitID,
+ CompareURL: compareURL,
+ Commits: commits,
+ Repo: apiRepo,
+ Pusher: apiPusher,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks.(new commit): %v", err)
+ }
+
+ action.OpType = ACTION_COMMIT_REPO
+ if err = NotifyWatchers(action); err != nil {
+ return fmt.Errorf("NotifyWatchers.(new commit): %v", err)
+ }
+
+ case ACTION_PUSH_TAG: // Tag
+ if isDelRef {
+ if err = PrepareWebhooks(repo, HOOK_EVENT_DELETE, &api.DeletePayload{
+ Ref: refName,
+ RefType: "tag",
+ PusherType: api.PUSHER_TYPE_USER,
+ Repo: apiRepo,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks.(delete tag): %v", err)
+ }
+
+ action.OpType = ACTION_DELETE_TAG
+ if err = NotifyWatchers(action); err != nil {
+ return fmt.Errorf("NotifyWatchers.(delete tag): %v", err)
+ }
+ return nil
+ }
+
+ if err = PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
+ Ref: refName,
+ RefType: "tag",
+ Sha: opts.NewCommitID,
+ DefaultBranch: repo.DefaultBranch,
+ Repo: apiRepo,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks.(new tag): %v", err)
+ }
+
+ action.OpType = ACTION_PUSH_TAG
+ if err = NotifyWatchers(action); err != nil {
+ return fmt.Errorf("NotifyWatchers.(new tag): %v", err)
+ }
+ }
+
+ return nil
+}
+
+func transferRepoAction(e Engine, doer, oldOwner *User, repo *Repository) (err error) {
+ if err = notifyWatchers(e, &Action{
+ ActUserID: doer.ID,
+ ActUserName: doer.Name,
+ OpType: ACTION_TRANSFER_REPO,
+ RepoID: repo.ID,
+ RepoUserName: repo.Owner.Name,
+ RepoName: repo.Name,
+ IsPrivate: repo.IsPrivate,
+ Content: path.Join(oldOwner.Name, repo.Name),
+ }); err != nil {
+ return fmt.Errorf("notifyWatchers: %v", err)
+ }
+
+ // Remove watch for organization.
+ if oldOwner.IsOrganization() {
+ if err = watchRepo(e, oldOwner.ID, repo.ID, false); err != nil {
+ return fmt.Errorf("watchRepo [false]: %v", err)
+ }
+ }
+
+ return nil
+}
+
+// TransferRepoAction adds new action for transferring repository,
+// the Owner field of repository is assumed to be new owner.
+func TransferRepoAction(doer, oldOwner *User, repo *Repository) error {
+ return transferRepoAction(x, doer, oldOwner, repo)
+}
+
+func mergePullRequestAction(e Engine, doer *User, repo *Repository, issue *Issue) error {
+ return notifyWatchers(e, &Action{
+ ActUserID: doer.ID,
+ ActUserName: doer.Name,
+ OpType: ACTION_MERGE_PULL_REQUEST,
+ Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
+ RepoID: repo.ID,
+ RepoUserName: repo.Owner.Name,
+ RepoName: repo.Name,
+ IsPrivate: repo.IsPrivate,
+ })
+}
+
+// MergePullRequestAction adds new action for merging pull request.
+func MergePullRequestAction(actUser *User, repo *Repository, pull *Issue) error {
+ return mergePullRequestAction(x, actUser, repo, pull)
+}
+
+func mirrorSyncAction(opType ActionType, repo *Repository, refName string, data []byte) error {
+ return NotifyWatchers(&Action{
+ ActUserID: repo.OwnerID,
+ ActUserName: repo.MustOwner().Name,
+ OpType: opType,
+ Content: string(data),
+ RepoID: repo.ID,
+ RepoUserName: repo.MustOwner().Name,
+ RepoName: repo.Name,
+ RefName: refName,
+ IsPrivate: repo.IsPrivate,
+ })
+}
+
+type MirrorSyncPushActionOptions struct {
+ RefName string
+ OldCommitID string
+ NewCommitID string
+ Commits *PushCommits
+}
+
+// MirrorSyncPushAction adds new action for mirror synchronization of pushed commits.
+func MirrorSyncPushAction(repo *Repository, opts MirrorSyncPushActionOptions) error {
+ if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum {
+ opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum]
+ }
+
+ apiCommits, err := opts.Commits.ToApiPayloadCommits(repo.RepoPath(), repo.HTMLURL())
+ if err != nil {
+ return fmt.Errorf("ToApiPayloadCommits: %v", err)
+ }
+
+ opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
+ apiPusher := repo.MustOwner().APIFormat()
+ if err := PrepareWebhooks(repo, HOOK_EVENT_PUSH, &api.PushPayload{
+ Ref: opts.RefName,
+ Before: opts.OldCommitID,
+ After: opts.NewCommitID,
+ CompareURL: setting.AppURL + opts.Commits.CompareURL,
+ Commits: apiCommits,
+ Repo: repo.APIFormat(nil),
+ Pusher: apiPusher,
+ Sender: apiPusher,
+ }); err != nil {
+ return fmt.Errorf("PrepareWebhooks: %v", err)
+ }
+
+ data, err := jsoniter.Marshal(opts.Commits)
+ if err != nil {
+ return err
+ }
+
+ return mirrorSyncAction(ACTION_MIRROR_SYNC_PUSH, repo, opts.RefName, data)
+}
+
+// MirrorSyncCreateAction adds new action for mirror synchronization of new reference.
+func MirrorSyncCreateAction(repo *Repository, refName string) error {
+ return mirrorSyncAction(ACTION_MIRROR_SYNC_CREATE, repo, refName, nil)
+}
+
+// MirrorSyncCreateAction adds new action for mirror synchronization of delete reference.
+func MirrorSyncDeleteAction(repo *Repository, refName string) error {
+ return mirrorSyncAction(ACTION_MIRROR_SYNC_DELETE, repo, refName, nil)
+}
+
+// GetFeeds returns action list of given user in given context.
+// actorID is the user who's requesting, ctxUserID is the user/org that is requested.
+// actorID can be -1 when isProfile is true or to skip the permission check.
+func GetFeeds(ctxUser *User, actorID, afterID int64, isProfile bool) ([]*Action, error) {
+ actions := make([]*Action, 0, setting.UI.User.NewsFeedPagingNum)
+ sess := x.Limit(setting.UI.User.NewsFeedPagingNum).Where("user_id = ?", ctxUser.ID).Desc("id")
+ if afterID > 0 {
+ sess.And("id < ?", afterID)
+ }
+ if isProfile {
+ sess.And("is_private = ?", false).And("act_user_id = ?", ctxUser.ID)
+ } else if actorID != -1 && ctxUser.IsOrganization() {
+ // FIXME: only need to get IDs here, not all fields of repository.
+ repos, _, err := ctxUser.GetUserRepositories(actorID, 1, ctxUser.NumRepos)
+ if err != nil {
+ return nil, fmt.Errorf("GetUserRepositories: %v", err)
+ }
+
+ var repoIDs []int64
+ for _, repo := range repos {
+ repoIDs = append(repoIDs, repo.ID)
+ }
+
+ if len(repoIDs) > 0 {
+ sess.In("repo_id", repoIDs)
+ }
+ }
+
+ err := sess.Find(&actions)
+ return actions, err
+}