|
@@ -0,0 +1,962 @@
|
|
|
|
+// Copyright 2020 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 (
|
|
|
|
+ "context"
|
|
|
|
+ "fmt"
|
|
|
|
+ "path"
|
|
|
|
+ "strconv"
|
|
|
|
+ "strings"
|
|
|
|
+ "time"
|
|
|
|
+ "unicode"
|
|
|
|
+
|
|
|
|
+ "github.com/gogs/git-module"
|
|
|
|
+ api "github.com/gogs/go-gogs-client"
|
|
|
|
+ jsoniter "github.com/json-iterator/go"
|
|
|
|
+ "github.com/pkg/errors"
|
|
|
|
+ "gorm.io/gorm"
|
|
|
|
+ log "unknwon.dev/clog/v2"
|
|
|
|
+
|
|
|
|
+ "gogs.io/gogs/internal/conf"
|
|
|
|
+ "gogs.io/gogs/internal/lazyregexp"
|
|
|
|
+ "gogs.io/gogs/internal/repoutil"
|
|
|
|
+ "gogs.io/gogs/internal/strutil"
|
|
|
|
+ "gogs.io/gogs/internal/testutil"
|
|
|
|
+ "gogs.io/gogs/internal/tool"
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
+// ActionsStore is the persistent interface for actions.
|
|
|
|
+//
|
|
|
|
+// NOTE: All methods are sorted in alphabetical order.
|
|
|
|
+type ActionsStore interface {
|
|
|
|
+ // CommitRepo creates actions for pushing commits to the repository. An action
|
|
|
|
+ // with the type ActionDeleteBranch is created if the push deletes a branch; an
|
|
|
|
+ // action with the type ActionCommitRepo is created for a regular push. If the
|
|
|
|
+ // regular push also creates a new branch, then another action with type
|
|
|
|
+ // ActionCreateBranch is created.
|
|
|
|
+ CommitRepo(ctx context.Context, opts CommitRepoOptions) error
|
|
|
|
+ // ListByOrganization returns actions of the organization viewable by the actor.
|
|
|
|
+ // Results are paginated if `afterID` is given.
|
|
|
|
+ ListByOrganization(ctx context.Context, orgID, actorID, afterID int64) ([]*Action, error)
|
|
|
|
+ // ListByUser returns actions of the user viewable by the actor. Results are
|
|
|
|
+ // paginated if `afterID` is given. The `isProfile` indicates whether repository
|
|
|
|
+ // permissions should be considered.
|
|
|
|
+ ListByUser(ctx context.Context, userID, actorID, afterID int64, isProfile bool) ([]*Action, error)
|
|
|
|
+ // MergePullRequest creates an action for merging a pull request.
|
|
|
|
+ MergePullRequest(ctx context.Context, doer, owner *User, repo *Repository, pull *Issue) error
|
|
|
|
+ // MirrorSyncCreate creates an action for mirror synchronization of a new
|
|
|
|
+ // reference.
|
|
|
|
+ MirrorSyncCreate(ctx context.Context, owner *User, repo *Repository, refName string) error
|
|
|
|
+ // MirrorSyncDelete creates an action for mirror synchronization of a reference
|
|
|
|
+ // deletion.
|
|
|
|
+ MirrorSyncDelete(ctx context.Context, owner *User, repo *Repository, refName string) error
|
|
|
|
+ // MirrorSyncPush creates an action for mirror synchronization of pushed
|
|
|
|
+ // commits.
|
|
|
|
+ MirrorSyncPush(ctx context.Context, opts MirrorSyncPushOptions) error
|
|
|
|
+ // NewRepo creates an action for creating a new repository. The action type
|
|
|
|
+ // could be ActionCreateRepo or ActionForkRepo based on whether the repository
|
|
|
|
+ // is a fork.
|
|
|
|
+ NewRepo(ctx context.Context, doer, owner *User, repo *Repository) error
|
|
|
|
+ // PushTag creates an action for pushing tags to the repository. An action with
|
|
|
|
+ // the type ActionDeleteTag is created if the push deletes a tag. Otherwise, an
|
|
|
|
+ // action with the type ActionPushTag is created for a regular push.
|
|
|
|
+ PushTag(ctx context.Context, opts PushTagOptions) error
|
|
|
|
+ // RenameRepo creates an action for renaming a repository.
|
|
|
|
+ RenameRepo(ctx context.Context, doer, owner *User, oldRepoName string, repo *Repository) error
|
|
|
|
+ // TransferRepo creates an action for transferring a repository to a new owner.
|
|
|
|
+ TransferRepo(ctx context.Context, doer, oldOwner, newOwner *User, repo *Repository) error
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+var Actions ActionsStore
|
|
|
|
+
|
|
|
|
+var _ ActionsStore = (*actions)(nil)
|
|
|
|
+
|
|
|
|
+type actions struct {
|
|
|
|
+ *gorm.DB
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// NewActionsStore returns a persistent interface for actions with given
|
|
|
|
+// database connection.
|
|
|
|
+func NewActionsStore(db *gorm.DB) ActionsStore {
|
|
|
|
+ return &actions{DB: db}
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) listByOrganization(ctx context.Context, orgID, actorID, afterID int64) *gorm.DB {
|
|
|
|
+ /*
|
|
|
|
+ Equivalent SQL for PostgreSQL:
|
|
|
|
+
|
|
|
|
+ SELECT * FROM "action"
|
|
|
|
+ WHERE
|
|
|
|
+ user_id = @userID
|
|
|
|
+ AND (@skipAfter OR id < @afterID)
|
|
|
|
+ AND repo_id IN (
|
|
|
|
+ SELECT repository.id FROM "repository"
|
|
|
|
+ JOIN team_repo ON repository.id = team_repo.repo_id
|
|
|
|
+ WHERE team_repo.team_id IN (
|
|
|
|
+ SELECT team_id FROM "team_user"
|
|
|
|
+ WHERE
|
|
|
|
+ team_user.org_id = @orgID AND uid = @actorID)
|
|
|
|
+ OR (repository.is_private = FALSE AND repository.is_unlisted = FALSE)
|
|
|
|
+ )
|
|
|
|
+ ORDER BY id DESC
|
|
|
|
+ LIMIT @limit
|
|
|
|
+ */
|
|
|
|
+ return db.WithContext(ctx).
|
|
|
|
+ Where("user_id = ?", orgID).
|
|
|
|
+ Where(db.
|
|
|
|
+ // Not apply when afterID is not given
|
|
|
|
+ Where("?", afterID <= 0).
|
|
|
|
+ Or("id < ?", afterID),
|
|
|
|
+ ).
|
|
|
|
+ Where("repo_id IN (?)",
|
|
|
|
+ db.Select("repository.id").
|
|
|
|
+ Table("repository").
|
|
|
|
+ Joins("JOIN team_repo ON repository.id = team_repo.repo_id").
|
|
|
|
+ Where("team_repo.team_id IN (?)",
|
|
|
|
+ db.Select("team_id").
|
|
|
|
+ Table("team_user").
|
|
|
|
+ Where("team_user.org_id = ? AND uid = ?", orgID, actorID),
|
|
|
|
+ ).
|
|
|
|
+ Or("repository.is_private = ? AND repository.is_unlisted = ?", false, false),
|
|
|
|
+ ).
|
|
|
|
+ Limit(conf.UI.User.NewsFeedPagingNum).
|
|
|
|
+ Order("id DESC")
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) ListByOrganization(ctx context.Context, orgID, actorID, afterID int64) ([]*Action, error) {
|
|
|
|
+ actions := make([]*Action, 0, conf.UI.User.NewsFeedPagingNum)
|
|
|
|
+ return actions, db.listByOrganization(ctx, orgID, actorID, afterID).Find(&actions).Error
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) listByUser(ctx context.Context, userID, actorID, afterID int64, isProfile bool) *gorm.DB {
|
|
|
|
+ /*
|
|
|
|
+ Equivalent SQL for PostgreSQL:
|
|
|
|
+
|
|
|
|
+ SELECT * FROM "action"
|
|
|
|
+ WHERE
|
|
|
|
+ user_id = @userID
|
|
|
|
+ AND (@skipAfter OR id < @afterID)
|
|
|
|
+ AND (@includePrivate OR (is_private = FALSE AND act_user_id = @actorID))
|
|
|
|
+ ORDER BY id DESC
|
|
|
|
+ LIMIT @limit
|
|
|
|
+ */
|
|
|
|
+ return db.WithContext(ctx).
|
|
|
|
+ Where("user_id = ?", userID).
|
|
|
|
+ Where(db.
|
|
|
|
+ // Not apply when afterID is not given
|
|
|
|
+ Where("?", afterID <= 0).
|
|
|
|
+ Or("id < ?", afterID),
|
|
|
|
+ ).
|
|
|
|
+ Where(db.
|
|
|
|
+ // Not apply when in not profile page or the user is viewing own profile
|
|
|
|
+ Where("?", !isProfile || actorID == userID).
|
|
|
|
+ Or("is_private = ? AND act_user_id = ?", false, userID),
|
|
|
|
+ ).
|
|
|
|
+ Limit(conf.UI.User.NewsFeedPagingNum).
|
|
|
|
+ Order("id DESC")
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) ListByUser(ctx context.Context, userID, actorID, afterID int64, isProfile bool) ([]*Action, error) {
|
|
|
|
+ actions := make([]*Action, 0, conf.UI.User.NewsFeedPagingNum)
|
|
|
|
+ return actions, db.listByUser(ctx, userID, actorID, afterID, isProfile).Find(&actions).Error
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// notifyWatchers creates rows in action table for watchers who are able to see the action.
|
|
|
|
+func (db *actions) notifyWatchers(ctx context.Context, act *Action) error {
|
|
|
|
+ watches, err := NewWatchesStore(db.DB).ListByRepo(ctx, act.RepoID)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "list watches")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Clone returns a deep copy of the action with UserID assigned
|
|
|
|
+ clone := func(userID int64) *Action {
|
|
|
|
+ tmp := *act
|
|
|
|
+ tmp.UserID = userID
|
|
|
|
+ return &tmp
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Plus one for the actor
|
|
|
|
+ actions := make([]*Action, 0, len(watches)+1)
|
|
|
|
+ actions = append(actions, clone(act.ActUserID))
|
|
|
|
+
|
|
|
|
+ for _, watch := range watches {
|
|
|
|
+ if act.ActUserID == watch.UserID {
|
|
|
|
+ continue
|
|
|
|
+ }
|
|
|
|
+ actions = append(actions, clone(watch.UserID))
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return db.Create(actions).Error
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) NewRepo(ctx context.Context, doer, owner *User, repo *Repository) error {
|
|
|
|
+ opType := ActionCreateRepo
|
|
|
|
+ if repo.IsFork {
|
|
|
|
+ opType = ActionForkRepo
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return db.notifyWatchers(ctx,
|
|
|
|
+ &Action{
|
|
|
|
+ ActUserID: doer.ID,
|
|
|
|
+ ActUserName: doer.Name,
|
|
|
|
+ OpType: opType,
|
|
|
|
+ RepoID: repo.ID,
|
|
|
|
+ RepoUserName: owner.Name,
|
|
|
|
+ RepoName: repo.Name,
|
|
|
|
+ IsPrivate: repo.IsPrivate || repo.IsUnlisted,
|
|
|
|
+ },
|
|
|
|
+ )
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) RenameRepo(ctx context.Context, doer, owner *User, oldRepoName string, repo *Repository) error {
|
|
|
|
+ return db.notifyWatchers(ctx,
|
|
|
|
+ &Action{
|
|
|
|
+ ActUserID: doer.ID,
|
|
|
|
+ ActUserName: doer.Name,
|
|
|
|
+ OpType: ActionRenameRepo,
|
|
|
|
+ RepoID: repo.ID,
|
|
|
|
+ RepoUserName: owner.Name,
|
|
|
|
+ RepoName: repo.Name,
|
|
|
|
+ IsPrivate: repo.IsPrivate || repo.IsUnlisted,
|
|
|
|
+ Content: oldRepoName,
|
|
|
|
+ },
|
|
|
|
+ )
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) mirrorSyncAction(ctx context.Context, opType ActionType, owner *User, repo *Repository, refName string, content []byte) error {
|
|
|
|
+ return db.notifyWatchers(ctx,
|
|
|
|
+ &Action{
|
|
|
|
+ ActUserID: owner.ID,
|
|
|
|
+ ActUserName: owner.Name,
|
|
|
|
+ OpType: opType,
|
|
|
|
+ Content: string(content),
|
|
|
|
+ RepoID: repo.ID,
|
|
|
|
+ RepoUserName: owner.Name,
|
|
|
|
+ RepoName: repo.Name,
|
|
|
|
+ RefName: refName,
|
|
|
|
+ IsPrivate: repo.IsPrivate || repo.IsUnlisted,
|
|
|
|
+ },
|
|
|
|
+ )
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+type MirrorSyncPushOptions struct {
|
|
|
|
+ Owner *User
|
|
|
|
+ Repo *Repository
|
|
|
|
+ RefName string
|
|
|
|
+ OldCommitID string
|
|
|
|
+ NewCommitID string
|
|
|
|
+ Commits *PushCommits
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) MirrorSyncPush(ctx context.Context, opts MirrorSyncPushOptions) error {
|
|
|
|
+ if conf.UI.FeedMaxCommitNum > 0 && len(opts.Commits.Commits) > conf.UI.FeedMaxCommitNum {
|
|
|
|
+ opts.Commits.Commits = opts.Commits.Commits[:conf.UI.FeedMaxCommitNum]
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ apiCommits, err := opts.Commits.APIFormat(ctx,
|
|
|
|
+ NewUsersStore(db.DB),
|
|
|
|
+ repoutil.RepositoryPath(opts.Owner.Name, opts.Repo.Name),
|
|
|
|
+ repoutil.HTMLURL(opts.Owner.Name, opts.Repo.Name),
|
|
|
|
+ )
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "convert commits to API format")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ opts.Commits.CompareURL = repoutil.CompareCommitsPath(opts.Owner.Name, opts.Repo.Name, opts.OldCommitID, opts.NewCommitID)
|
|
|
|
+ apiPusher := opts.Owner.APIFormat()
|
|
|
|
+ err = PrepareWebhooks(
|
|
|
|
+ opts.Repo,
|
|
|
|
+ HOOK_EVENT_PUSH,
|
|
|
|
+ &api.PushPayload{
|
|
|
|
+ Ref: opts.RefName,
|
|
|
|
+ Before: opts.OldCommitID,
|
|
|
|
+ After: opts.NewCommitID,
|
|
|
|
+ CompareURL: conf.Server.ExternalURL + opts.Commits.CompareURL,
|
|
|
|
+ Commits: apiCommits,
|
|
|
|
+ Repo: opts.Repo.APIFormat(opts.Owner),
|
|
|
|
+ Pusher: apiPusher,
|
|
|
|
+ Sender: apiPusher,
|
|
|
|
+ },
|
|
|
|
+ )
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "prepare webhooks")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ data, err := jsoniter.Marshal(opts.Commits)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "marshal JSON")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return db.mirrorSyncAction(ctx, ActionMirrorSyncPush, opts.Owner, opts.Repo, opts.RefName, data)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) MirrorSyncCreate(ctx context.Context, owner *User, repo *Repository, refName string) error {
|
|
|
|
+ return db.mirrorSyncAction(ctx, ActionMirrorSyncCreate, owner, repo, refName, nil)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) MirrorSyncDelete(ctx context.Context, owner *User, repo *Repository, refName string) error {
|
|
|
|
+ return db.mirrorSyncAction(ctx, ActionMirrorSyncDelete, owner, repo, refName, nil)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) MergePullRequest(ctx context.Context, doer, owner *User, repo *Repository, pull *Issue) error {
|
|
|
|
+ return db.notifyWatchers(ctx,
|
|
|
|
+ &Action{
|
|
|
|
+ ActUserID: doer.ID,
|
|
|
|
+ ActUserName: doer.Name,
|
|
|
|
+ OpType: ActionMergePullRequest,
|
|
|
|
+ Content: fmt.Sprintf("%d|%s", pull.Index, pull.Title),
|
|
|
|
+ RepoID: repo.ID,
|
|
|
|
+ RepoUserName: owner.Name,
|
|
|
|
+ RepoName: repo.Name,
|
|
|
|
+ IsPrivate: repo.IsPrivate || repo.IsUnlisted,
|
|
|
|
+ },
|
|
|
|
+ )
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) TransferRepo(ctx context.Context, doer, oldOwner, newOwner *User, repo *Repository) error {
|
|
|
|
+ return db.notifyWatchers(ctx,
|
|
|
|
+ &Action{
|
|
|
|
+ ActUserID: doer.ID,
|
|
|
|
+ ActUserName: doer.Name,
|
|
|
|
+ OpType: ActionTransferRepo,
|
|
|
|
+ RepoID: repo.ID,
|
|
|
|
+ RepoUserName: newOwner.Name,
|
|
|
|
+ RepoName: repo.Name,
|
|
|
|
+ IsPrivate: repo.IsPrivate || repo.IsUnlisted,
|
|
|
|
+ Content: oldOwner.Name + "/" + repo.Name,
|
|
|
|
+ },
|
|
|
|
+ )
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+var (
|
|
|
|
+ // Same as GitHub, see https://docs.github.com/en/free-pro-team@latest/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue
|
|
|
|
+ issueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
|
|
|
|
+ issueReopenKeywords = []string{"reopen", "reopens", "reopened"}
|
|
|
|
+
|
|
|
|
+ issueCloseKeywordsPattern = lazyregexp.New(assembleKeywordsPattern(issueCloseKeywords))
|
|
|
|
+ issueReopenKeywordsPattern = lazyregexp.New(assembleKeywordsPattern(issueReopenKeywords))
|
|
|
|
+ issueReferencePattern = lazyregexp.New(`(?i)(?:)(^| )\S*#\d+`)
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
+func assembleKeywordsPattern(words []string) string {
|
|
|
|
+ return fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(words, "|"))
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// updateCommitReferencesToIssues checks if issues are manipulated by commit message.
|
|
|
|
+func updateCommitReferencesToIssues(doer *User, repo *Repository, commits []*PushCommit) error {
|
|
|
|
+ trimRightNonDigits := func(c rune) bool {
|
|
|
|
+ return !unicode.IsDigit(c)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 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 issueReferencePattern.FindAllString(c.Message, -1) {
|
|
|
|
+ ref = strings.TrimSpace(ref)
|
|
|
|
+ ref = strings.TrimRightFunc(ref, trimRightNonDigits)
|
|
|
|
+
|
|
|
|
+ if ref == "" {
|
|
|
|
+ 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 IsErrIssueNotExist(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 and the next for loop to a common function.
|
|
|
|
+ for _, ref := range issueCloseKeywordsPattern.FindAllString(c.Message, -1) {
|
|
|
|
+ ref = ref[strings.IndexByte(ref, byte(' '))+1:]
|
|
|
|
+ ref = strings.TrimRightFunc(ref, trimRightNonDigits)
|
|
|
|
+
|
|
|
|
+ if ref == "" {
|
|
|
|
+ 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 IsErrIssueNotExist(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 issueReopenKeywordsPattern.FindAllString(c.Message, -1) {
|
|
|
|
+ ref = ref[strings.IndexByte(ref, byte(' '))+1:]
|
|
|
|
+ ref = strings.TrimRightFunc(ref, trimRightNonDigits)
|
|
|
|
+
|
|
|
|
+ if ref == "" {
|
|
|
|
+ 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 IsErrIssueNotExist(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 CommitRepoOptions struct {
|
|
|
|
+ Owner *User
|
|
|
|
+ Repo *Repository
|
|
|
|
+ PusherName string
|
|
|
|
+ RefFullName string
|
|
|
|
+ OldCommitID string
|
|
|
|
+ NewCommitID string
|
|
|
|
+ Commits *PushCommits
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) CommitRepo(ctx context.Context, opts CommitRepoOptions) error {
|
|
|
|
+ err := NewReposStore(db.DB).Touch(ctx, opts.Repo.ID)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "touch repository")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pusher, err := NewUsersStore(db.DB).GetByUsername(ctx, opts.PusherName)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrapf(err, "get pusher [name: %s]", opts.PusherName)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ isNewRef := opts.OldCommitID == git.EmptyID
|
|
|
|
+ isDelRef := opts.NewCommitID == git.EmptyID
|
|
|
|
+
|
|
|
|
+ // If not the first commit, set the compare URL.
|
|
|
|
+ if !isNewRef && !isDelRef {
|
|
|
|
+ opts.Commits.CompareURL = repoutil.CompareCommitsPath(opts.Owner.Name, opts.Repo.Name, opts.OldCommitID, opts.NewCommitID)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ refName := git.RefShortName(opts.RefFullName)
|
|
|
|
+ action := &Action{
|
|
|
|
+ ActUserID: pusher.ID,
|
|
|
|
+ ActUserName: pusher.Name,
|
|
|
|
+ RepoID: opts.Repo.ID,
|
|
|
|
+ RepoUserName: opts.Owner.Name,
|
|
|
|
+ RepoName: opts.Repo.Name,
|
|
|
|
+ RefName: refName,
|
|
|
|
+ IsPrivate: opts.Repo.IsPrivate || opts.Repo.IsUnlisted,
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ apiRepo := opts.Repo.APIFormat(opts.Owner)
|
|
|
|
+ apiPusher := pusher.APIFormat()
|
|
|
|
+ if isDelRef {
|
|
|
|
+ err = PrepareWebhooks(
|
|
|
|
+ opts.Repo,
|
|
|
|
+ HOOK_EVENT_DELETE,
|
|
|
|
+ &api.DeletePayload{
|
|
|
|
+ Ref: refName,
|
|
|
|
+ RefType: "branch",
|
|
|
|
+ PusherType: api.PUSHER_TYPE_USER,
|
|
|
|
+ Repo: apiRepo,
|
|
|
|
+ Sender: apiPusher,
|
|
|
|
+ },
|
|
|
|
+ )
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "prepare webhooks for delete branch")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ action.OpType = ActionDeleteBranch
|
|
|
|
+ err = db.notifyWatchers(ctx, action)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "notify watchers")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Delete branch doesn't have anything to push or compare
|
|
|
|
+ return nil
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Only update issues via commits when internal issue tracker is enabled
|
|
|
|
+ if opts.Repo.EnableIssues && !opts.Repo.EnableExternalTracker {
|
|
|
|
+ if err = updateCommitReferencesToIssues(pusher, opts.Repo, opts.Commits.Commits); err != nil {
|
|
|
|
+ log.Error("update commit references to issues: %v", err)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ if conf.UI.FeedMaxCommitNum > 0 && len(opts.Commits.Commits) > conf.UI.FeedMaxCommitNum {
|
|
|
|
+ opts.Commits.Commits = opts.Commits.Commits[:conf.UI.FeedMaxCommitNum]
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ data, err := jsoniter.Marshal(opts.Commits)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "marshal JSON")
|
|
|
|
+ }
|
|
|
|
+ action.Content = string(data)
|
|
|
|
+
|
|
|
|
+ var compareURL string
|
|
|
|
+ if isNewRef {
|
|
|
|
+ err = PrepareWebhooks(
|
|
|
|
+ opts.Repo,
|
|
|
|
+ HOOK_EVENT_CREATE,
|
|
|
|
+ &api.CreatePayload{
|
|
|
|
+ Ref: refName,
|
|
|
|
+ RefType: "branch",
|
|
|
|
+ DefaultBranch: opts.Repo.DefaultBranch,
|
|
|
|
+ Repo: apiRepo,
|
|
|
|
+ Sender: apiPusher,
|
|
|
|
+ },
|
|
|
|
+ )
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "prepare webhooks for new branch")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ action.OpType = ActionCreateBranch
|
|
|
|
+ err = db.notifyWatchers(ctx, action)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "notify watchers")
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ compareURL = conf.Server.ExternalURL + opts.Commits.CompareURL
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ commits, err := opts.Commits.APIFormat(ctx,
|
|
|
|
+ NewUsersStore(db.DB),
|
|
|
|
+ repoutil.RepositoryPath(opts.Owner.Name, opts.Repo.Name),
|
|
|
|
+ repoutil.HTMLURL(opts.Owner.Name, opts.Repo.Name),
|
|
|
|
+ )
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "convert commits to API format")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ err = PrepareWebhooks(
|
|
|
|
+ opts.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,
|
|
|
|
+ },
|
|
|
|
+ )
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "prepare webhooks for new commit")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ action.OpType = ActionCommitRepo
|
|
|
|
+ err = db.notifyWatchers(ctx, action)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "notify watchers")
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+type PushTagOptions struct {
|
|
|
|
+ Owner *User
|
|
|
|
+ Repo *Repository
|
|
|
|
+ PusherName string
|
|
|
|
+ RefFullName string
|
|
|
|
+ NewCommitID string
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (db *actions) PushTag(ctx context.Context, opts PushTagOptions) error {
|
|
|
|
+ err := NewReposStore(db.DB).Touch(ctx, opts.Repo.ID)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "touch repository")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ pusher, err := NewUsersStore(db.DB).GetByUsername(ctx, opts.PusherName)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrapf(err, "get pusher [name: %s]", opts.PusherName)
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ refName := git.RefShortName(opts.RefFullName)
|
|
|
|
+ action := &Action{
|
|
|
|
+ ActUserID: pusher.ID,
|
|
|
|
+ ActUserName: pusher.Name,
|
|
|
|
+ RepoID: opts.Repo.ID,
|
|
|
|
+ RepoUserName: opts.Owner.Name,
|
|
|
|
+ RepoName: opts.Repo.Name,
|
|
|
|
+ RefName: refName,
|
|
|
|
+ IsPrivate: opts.Repo.IsPrivate || opts.Repo.IsUnlisted,
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ apiRepo := opts.Repo.APIFormat(opts.Owner)
|
|
|
|
+ apiPusher := pusher.APIFormat()
|
|
|
|
+ if opts.NewCommitID == git.EmptyID {
|
|
|
|
+ err = PrepareWebhooks(
|
|
|
|
+ opts.Repo,
|
|
|
|
+ HOOK_EVENT_DELETE,
|
|
|
|
+ &api.DeletePayload{
|
|
|
|
+ Ref: refName,
|
|
|
|
+ RefType: "tag",
|
|
|
|
+ PusherType: api.PUSHER_TYPE_USER,
|
|
|
|
+ Repo: apiRepo,
|
|
|
|
+ Sender: apiPusher,
|
|
|
|
+ },
|
|
|
|
+ )
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "prepare webhooks for delete tag")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ action.OpType = ActionDeleteTag
|
|
|
|
+ err = db.notifyWatchers(ctx, action)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "notify watchers")
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ err = PrepareWebhooks(
|
|
|
|
+ opts.Repo,
|
|
|
|
+ HOOK_EVENT_CREATE,
|
|
|
|
+ &api.CreatePayload{
|
|
|
|
+ Ref: refName,
|
|
|
|
+ RefType: "tag",
|
|
|
|
+ Sha: opts.NewCommitID,
|
|
|
|
+ DefaultBranch: opts.Repo.DefaultBranch,
|
|
|
|
+ Repo: apiRepo,
|
|
|
|
+ Sender: apiPusher,
|
|
|
|
+ },
|
|
|
|
+ )
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrapf(err, "prepare webhooks for new tag")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ action.OpType = ActionPushTag
|
|
|
|
+ err = db.notifyWatchers(ctx, action)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return errors.Wrap(err, "notify watchers")
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// ActionType is the type of an action.
|
|
|
|
+type ActionType int
|
|
|
|
+
|
|
|
|
+// ⚠️ WARNING: Only append to the end of list to maintain backward compatibility.
|
|
|
|
+const (
|
|
|
|
+ ActionCreateRepo ActionType = iota + 1 // 1
|
|
|
|
+ ActionRenameRepo // 2
|
|
|
|
+ ActionStarRepo // 3
|
|
|
|
+ ActionWatchRepo // 4
|
|
|
|
+ ActionCommitRepo // 5
|
|
|
|
+ ActionCreateIssue // 6
|
|
|
|
+ ActionCreatePullRequest // 7
|
|
|
|
+ ActionTransferRepo // 8
|
|
|
|
+ ActionPushTag // 9
|
|
|
|
+ ActionCommentIssue // 10
|
|
|
|
+ ActionMergePullRequest // 11
|
|
|
|
+ ActionCloseIssue // 12
|
|
|
|
+ ActionReopenIssue // 13
|
|
|
|
+ ActionClosePullRequest // 14
|
|
|
|
+ ActionReopenPullRequest // 15
|
|
|
|
+ ActionCreateBranch // 16
|
|
|
|
+ ActionDeleteBranch // 17
|
|
|
|
+ ActionDeleteTag // 18
|
|
|
|
+ ActionForkRepo // 19
|
|
|
|
+ ActionMirrorSyncPush // 20
|
|
|
|
+ ActionMirrorSyncCreate // 21
|
|
|
|
+ ActionMirrorSyncDelete // 22
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
+// Action is a user operation to a repository. It implements template.Actioner
|
|
|
|
+// interface to be able to use it in template rendering.
|
|
|
|
+type Action struct {
|
|
|
|
+ ID int64 `gorm:"primaryKey"`
|
|
|
|
+ UserID int64 `gorm:"index"` // Receiver user ID
|
|
|
|
+ OpType ActionType
|
|
|
|
+ ActUserID int64 // Doer user ID
|
|
|
|
+ ActUserName string // Doer user name
|
|
|
|
+ ActAvatar string `xorm:"-" gorm:"-" json:"-"`
|
|
|
|
+ RepoID int64 `xorm:"INDEX" gorm:"index"`
|
|
|
|
+ RepoUserName string
|
|
|
|
+ RepoName string
|
|
|
|
+ RefName string
|
|
|
|
+ IsPrivate bool `xorm:"NOT NULL DEFAULT false" gorm:"not null;default:FALSE"`
|
|
|
|
+ Content string `xorm:"TEXT"`
|
|
|
|
+
|
|
|
|
+ Created time.Time `xorm:"-" gorm:"-" json:"-"`
|
|
|
|
+ CreatedUnix int64
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// BeforeCreate implements the GORM create hook.
|
|
|
|
+func (a *Action) BeforeCreate(tx *gorm.DB) error {
|
|
|
|
+ if a.CreatedUnix <= 0 {
|
|
|
|
+ a.CreatedUnix = tx.NowFunc().Unix()
|
|
|
|
+ }
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// AfterFind implements the GORM query hook.
|
|
|
|
+func (a *Action) AfterFind(_ *gorm.DB) error {
|
|
|
|
+ a.Created = time.Unix(a.CreatedUnix, 0).Local()
|
|
|
|
+ return nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (a *Action) GetOpType() int {
|
|
|
|
+ return int(a.OpType)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (a *Action) GetActUserName() string {
|
|
|
|
+ return a.ActUserName
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (a *Action) ShortActUserName() string {
|
|
|
|
+ return strutil.Ellipsis(a.ActUserName, 20)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (a *Action) GetRepoUserName() string {
|
|
|
|
+ return a.RepoUserName
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (a *Action) ShortRepoUserName() string {
|
|
|
|
+ return strutil.Ellipsis(a.RepoUserName, 20)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (a *Action) GetRepoName() string {
|
|
|
|
+ return a.RepoName
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (a *Action) ShortRepoName() string {
|
|
|
|
+ return strutil.Ellipsis(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 conf.Server.Subpath != "" {
|
|
|
|
+ return path.Join(conf.Server.Subpath, 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, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
|
|
|
|
+ issue, err := GetIssueByIndex(a.RepoID, index)
|
|
|
|
+ if err != nil {
|
|
|
|
+ log.Error("Failed to get issue title [repo_id: %d, index: %d]: %v", a.RepoID, index, err)
|
|
|
|
+ return "error getting issue"
|
|
|
|
+ }
|
|
|
|
+ return issue.Title
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (a *Action) GetIssueContent() string {
|
|
|
|
+ index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
|
|
|
|
+ issue, err := GetIssueByIndex(a.RepoID, index)
|
|
|
|
+ if err != nil {
|
|
|
|
+ log.Error("Failed to get issue content [repo_id: %d, index: %d]: %v", a.RepoID, index, err)
|
|
|
|
+ return "error getting issue"
|
|
|
|
+ }
|
|
|
|
+ return issue.Content
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// PushCommit contains information of a pushed commit.
|
|
|
|
+type PushCommit struct {
|
|
|
|
+ Sha1 string
|
|
|
|
+ Message string
|
|
|
|
+ AuthorEmail string
|
|
|
|
+ AuthorName string
|
|
|
|
+ CommitterEmail string
|
|
|
|
+ CommitterName string
|
|
|
|
+ Timestamp time.Time
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// PushCommits is a list of pushed commits.
|
|
|
|
+type PushCommits struct {
|
|
|
|
+ Len int
|
|
|
|
+ Commits []*PushCommit
|
|
|
|
+ CompareURL string
|
|
|
|
+
|
|
|
|
+ avatars map[string]string
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// NewPushCommits returns a new PushCommits.
|
|
|
|
+func NewPushCommits() *PushCommits {
|
|
|
|
+ return &PushCommits{
|
|
|
|
+ avatars: make(map[string]string),
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+func (pcs *PushCommits) APIFormat(ctx context.Context, usersStore UsersStore, repoPath, repoURL string) ([]*api.PayloadCommit, error) {
|
|
|
|
+ // NOTE: We cache query results in case there are many commits in a single push.
|
|
|
|
+ usernameByEmail := make(map[string]string)
|
|
|
|
+ getUsernameByEmail := func(email string) (string, error) {
|
|
|
|
+ username, ok := usernameByEmail[email]
|
|
|
|
+ if ok {
|
|
|
|
+ return username, nil
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ user, err := usersStore.GetByEmail(ctx, email)
|
|
|
|
+ if err != nil {
|
|
|
|
+ if IsErrUserNotExist(err) {
|
|
|
|
+ usernameByEmail[email] = ""
|
|
|
|
+ return "", nil
|
|
|
|
+ }
|
|
|
|
+ return "", err
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ usernameByEmail[email] = user.Name
|
|
|
|
+ return user.Name, nil
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ commits := make([]*api.PayloadCommit, len(pcs.Commits))
|
|
|
|
+ for i, commit := range pcs.Commits {
|
|
|
|
+ authorUsername, err := getUsernameByEmail(commit.AuthorEmail)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return nil, errors.Wrap(err, "get author username")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ committerUsername, err := getUsernameByEmail(commit.CommitterEmail)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return nil, errors.Wrap(err, "get committer username")
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ nameStatus := &git.NameStatus{}
|
|
|
|
+ if !testutil.InTest {
|
|
|
|
+ nameStatus, err = git.ShowNameStatus(repoPath, commit.Sha1)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return nil, errors.Wrapf(err, "show name status [commit_sha1: %s]", commit.Sha1)
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ 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: nameStatus.Added,
|
|
|
|
+ Removed: nameStatus.Removed,
|
|
|
|
+ Modified: nameStatus.Modified,
|
|
|
|
+ Timestamp: commit.Timestamp,
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ return commits, nil
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// AvatarLink tries to match user in database with email in order to show custom
|
|
|
|
+// avatars, and falls back to general avatar link.
|
|
|
|
+//
|
|
|
|
+// FIXME: This method does not belong to PushCommits, should be a pure template
|
|
|
|
+// function.
|
|
|
|
+func (pcs *PushCommits) AvatarLink(email string) string {
|
|
|
|
+ _, ok := pcs.avatars[email]
|
|
|
|
+ if !ok {
|
|
|
|
+ u, err := Users.GetByEmail(context.Background(), email)
|
|
|
|
+ if err != nil {
|
|
|
|
+ pcs.avatars[email] = tool.AvatarLink(email)
|
|
|
|
+ if !IsErrUserNotExist(err) {
|
|
|
|
+ log.Error("Failed to get user [email: %s]: %v", email, err)
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ pcs.avatars[email] = u.RelAvatarLink()
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return pcs.avatars[email]
|
|
|
|
+}
|