123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962 |
- // 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.
- 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),
- ).
- Order("id DESC").
- Limit(conf.UI.User.NewsFeedPagingNum)
- }
- 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),
- ).
- Order("id DESC").
- Limit(conf.UI.User.NewsFeedPagingNum)
- }
- 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 := NewRepositoriesStore(db.DB).ListWatches(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 := NewRepositoriesStore(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 := NewRepositoriesStore(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.AvatarURLPath()
- }
- }
- return pcs.avatars[email]
- }
|