| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514 | // 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 modelsimport (	"encoding/json"	"errors"	"fmt"	"path"	"regexp"	"strings"	"time"	"unicode"	"github.com/gogits/gogs/modules/base"	"github.com/gogits/gogs/modules/git"	"github.com/gogits/gogs/modules/log"	"github.com/gogits/gogs/modules/setting")type ActionType intconst (	CREATE_REPO   ActionType = iota + 1 // 1	DELETE_REPO                         // 2	STAR_REPO                           // 3	FOLLOW_REPO                         // 4	COMMIT_REPO                         // 5	CREATE_ISSUE                        // 6	PULL_REQUEST                        // 7	TRANSFER_REPO                       // 8	PUSH_TAG                            // 9	COMMENT_ISSUE                       // 10)var (	ErrNotImplemented = errors.New("Not implemented yet"))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, IssueReopenKeywordsPat *regexp.Regexp	IssueReferenceKeywordsPat                     *regexp.Regexp)func assembleKeywordsPattern(words []string) string {	return fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(words, "|"))}func init() {	IssueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(IssueCloseKeywords))	IssueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(IssueReopenKeywords))	IssueReferenceKeywordsPat = regexp.MustCompile(`(?i)(?:)(^| )\S+`)}// 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 `xorm:"pk autoincr"`	UserID       int64 // Receiver user id.	OpType       ActionType	ActUserID    int64  // Action user id.	ActUserName  string // Action user name.	ActEmail     string	ActAvatar    string `xorm:"-"`	RepoID       int64	RepoUserName string	RepoName     string	RefName      string	IsPrivate    bool      `xorm:"NOT NULL DEFAULT false"`	Content      string    `xorm:"TEXT"`	Created      time.Time `xorm:"created"`}func (a Action) GetOpType() int {	return int(a.OpType)}func (a Action) GetActUserName() string {	return a.ActUserName}func (a Action) GetActEmail() string {	return a.ActEmail}func (a Action) GetRepoUserName() string {	return a.RepoUserName}func (a Action) GetRepoName() string {	return a.RepoName}func (a Action) GetRepoPath() string {	return path.Join(a.RepoUserName, a.RepoName)}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 updateIssuesCommit(userId, repoId int64, repoUserName, repoName string, commits []*base.PushCommit) error {	for _, c := range commits {		for _, ref := range IssueReferenceKeywordsPat.FindAllString(c.Message, -1) {			ref := ref[strings.IndexByte(ref, byte(' '))+1:]			ref = strings.TrimRightFunc(ref, func(c rune) bool {				return !unicode.IsDigit(c)			})			if len(ref) == 0 {				continue			}			// Add repo name if missing			if ref[0] == '#' {				ref = fmt.Sprintf("%s/%s%s", repoUserName, repoName, ref)			} else if strings.Contains(ref, "/") == false {				// FIXME: We don't support User#ID syntax yet				// return ErrNotImplemented				continue			}			issue, err := GetIssueByRef(ref)			if err != nil {				return err			}			url := fmt.Sprintf("%s/%s/%s/commit/%s", setting.AppSubUrl, repoUserName, repoName, c.Sha1)			message := fmt.Sprintf(`<a href="%s">%s</a>`, url, c.Message)			if _, err = CreateComment(userId, issue.RepoID, issue.ID, 0, 0, COMMENT_TYPE_COMMIT, message, nil); err != nil {				return err			}		}		for _, ref := range IssueCloseKeywordsPat.FindAllString(c.Message, -1) {			ref := ref[strings.IndexByte(ref, byte(' '))+1:]			ref = strings.TrimRightFunc(ref, func(c rune) bool {				return !unicode.IsDigit(c)			})			if len(ref) == 0 {				continue			}			// Add repo name if missing			if ref[0] == '#' {				ref = fmt.Sprintf("%s/%s%s", repoUserName, repoName, ref)			} else if strings.Contains(ref, "/") == false {				// We don't support User#ID syntax yet				// return ErrNotImplemented				continue			}			issue, err := GetIssueByRef(ref)			if err != nil {				return err			}			if issue.RepoID == repoId {				if issue.IsClosed {					continue				}				issue.IsClosed = true				if err = issue.GetLabels(); err != nil {					return err				}				for _, label := range issue.Labels {					label.NumClosedIssues++					if err = UpdateLabel(label); err != nil {						return err					}				}				if err = UpdateIssue(issue); err != nil {					return err				} else if err = UpdateIssueUserPairsByStatus(issue.ID, issue.IsClosed); err != nil {					return err				}				if err = ChangeMilestoneIssueStats(issue); err != nil {					return err				}				// If commit happened in the referenced repository, it means the issue can be closed.				if _, err = CreateComment(userId, repoId, issue.ID, 0, 0, COMMENT_TYPE_CLOSE, "", nil); err != nil {					return err				}			}		}		for _, ref := range IssueReopenKeywordsPat.FindAllString(c.Message, -1) {			ref := ref[strings.IndexByte(ref, byte(' '))+1:]			ref = strings.TrimRightFunc(ref, func(c rune) bool {				return !unicode.IsDigit(c)			})			if len(ref) == 0 {				continue			}			// Add repo name if missing			if ref[0] == '#' {				ref = fmt.Sprintf("%s/%s%s", repoUserName, repoName, ref)			} else if strings.Contains(ref, "/") == false {				// We don't support User#ID syntax yet				// return ErrNotImplemented				continue			}			issue, err := GetIssueByRef(ref)			if err != nil {				return err			}			if issue.RepoID == repoId {				if !issue.IsClosed {					continue				}				issue.IsClosed = false				if err = issue.GetLabels(); err != nil {					return err				}				for _, label := range issue.Labels {					label.NumClosedIssues--					if err = UpdateLabel(label); err != nil {						return err					}				}				if err = UpdateIssue(issue); err != nil {					return err				} else if err = UpdateIssueUserPairsByStatus(issue.ID, issue.IsClosed); err != nil {					return err				}				if err = ChangeMilestoneIssueStats(issue); err != nil {					return err				}				// If commit happened in the referenced repository, it means the issue can be closed.				if _, err = CreateComment(userId, repoId, issue.ID, 0, 0, COMMENT_TYPE_REOPEN, "", nil); err != nil {					return err				}			}		}	}	return nil}// CommitRepoAction adds new action for committing repository.func CommitRepoAction(userId, repoUserId int64, userName, actEmail string,	repoId int64, repoUserName, repoName string, refFullName string, commit *base.PushCommits, oldCommitId string, newCommitId string) error {	opType := COMMIT_REPO	// Check it's tag push or branch.	if strings.HasPrefix(refFullName, "refs/tags/") {		opType = PUSH_TAG		commit = &base.PushCommits{}	}	repoLink := fmt.Sprintf("%s%s/%s", setting.AppUrl, repoUserName, repoName)	// if not the first commit, set the compareUrl	if !strings.HasPrefix(oldCommitId, "0000000") {		commit.CompareUrl = fmt.Sprintf("%s/%s/compare/%s...%s", repoUserName, repoName, oldCommitId, newCommitId)	}	bs, err := json.Marshal(commit)	if err != nil {		return errors.New("json: " + err.Error())	}	refName := git.RefEndName(refFullName)	// Change repository bare status and update last updated time.	repo, err := GetRepositoryByName(repoUserId, repoName)	if err != nil {		return errors.New("GetRepositoryByName: " + err.Error())	}	repo.IsBare = false	if err = UpdateRepository(repo, false); err != nil {		return errors.New("UpdateRepository: " + err.Error())	}	err = updateIssuesCommit(userId, repoId, repoUserName, repoName, commit.Commits)	if err != nil {		log.Debug("updateIssuesCommit: ", err)	}	if err = NotifyWatchers(&Action{		ActUserID:    userId,		ActUserName:  userName,		ActEmail:     actEmail,		OpType:       opType,		Content:      string(bs),		RepoID:       repoId,		RepoUserName: repoUserName,		RepoName:     repoName,		RefName:      refName,		IsPrivate:    repo.IsPrivate,	}); err != nil {		return errors.New("NotifyWatchers: " + err.Error())	}	// New push event hook.	if err := repo.GetOwner(); err != nil {		return errors.New("GetOwner: " + err.Error())	}	ws, err := GetActiveWebhooksByRepoId(repoId)	if err != nil {		return errors.New("GetActiveWebhooksByRepoId: " + err.Error())	}	// check if repo belongs to org and append additional webhooks	if repo.Owner.IsOrganization() {		// get hooks for org		orgws, err := GetActiveWebhooksByOrgId(repo.OwnerId)		if err != nil {			return errors.New("GetActiveWebhooksByOrgId: " + err.Error())		}		ws = append(ws, orgws...)	}	if len(ws) == 0 {		return nil	}	pusher_email, pusher_name := "", ""	pusher, err := GetUserByName(userName)	if err == nil {		pusher_email = pusher.Email		pusher_name = pusher.GetFullNameFallback()	}	commits := make([]*PayloadCommit, len(commit.Commits))	for i, cmt := range commit.Commits {		author_username := ""		author, err := GetUserByEmail(cmt.AuthorEmail)		if err == nil {			author_username = author.Name		}		commits[i] = &PayloadCommit{			Id:      cmt.Sha1,			Message: cmt.Message,			Url:     fmt.Sprintf("%s/commit/%s", repoLink, cmt.Sha1),			Author: &PayloadAuthor{				Name:     cmt.AuthorName,				Email:    cmt.AuthorEmail,				UserName: author_username,			},		}	}	p := &Payload{		Ref:     refFullName,		Commits: commits,		Repo: &PayloadRepo{			Id:          repo.Id,			Name:        repo.LowerName,			Url:         repoLink,			Description: repo.Description,			Website:     repo.Website,			Watchers:    repo.NumWatches,			Owner: &PayloadAuthor{				Name:     repo.Owner.GetFullNameFallback(),				Email:    repo.Owner.Email,				UserName: repo.Owner.Name,			},			Private: repo.IsPrivate,		},		Pusher: &PayloadAuthor{			Name:     pusher_name,			Email:    pusher_email,			UserName: userName,		},		Before:     oldCommitId,		After:      newCommitId,		CompareUrl: setting.AppUrl + commit.CompareUrl,	}	for _, w := range ws {		w.GetEvent()		if !w.HasPushEvent() {			continue		}		var payload BasePayload		switch w.HookTaskType {		case SLACK:			s, err := GetSlackPayload(p, w.Meta)			if err != nil {				return errors.New("action.GetSlackPayload: " + err.Error())			}			payload = s		default:			payload = p			p.Secret = w.Secret		}		if err = CreateHookTask(&HookTask{			RepoID:      repo.Id,			HookID:      w.Id,			Type:        w.HookTaskType,			Url:         w.Url,			BasePayload: payload,			ContentType: w.ContentType,			EventType:   HOOK_EVENT_PUSH,			IsSsl:       w.IsSsl,		}); err != nil {			return fmt.Errorf("CreateHookTask: %v", err)		}	}	return nil}func newRepoAction(e Engine, u *User, repo *Repository) (err error) {	if err = notifyWatchers(e, &Action{		ActUserID:    u.Id,		ActUserName:  u.Name,		ActEmail:     u.Email,		OpType:       CREATE_REPO,		RepoID:       repo.Id,		RepoUserName: repo.Owner.Name,		RepoName:     repo.Name,		IsPrivate:    repo.IsPrivate,	}); err != nil {		return fmt.Errorf("notify watchers '%d/%s'", u.Id, repo.Id)	}	log.Trace("action.NewRepoAction: %s/%s", u.Name, repo.Name)	return err}// NewRepoAction adds new action for creating repository.func NewRepoAction(u *User, repo *Repository) (err error) {	return newRepoAction(x, u, repo)}func transferRepoAction(e Engine, actUser, oldOwner, newOwner *User, repo *Repository) (err error) {	action := &Action{		ActUserID:    actUser.Id,		ActUserName:  actUser.Name,		ActEmail:     actUser.Email,		OpType:       TRANSFER_REPO,		RepoID:       repo.Id,		RepoUserName: newOwner.Name,		RepoName:     repo.Name,		IsPrivate:    repo.IsPrivate,		Content:      path.Join(oldOwner.LowerName, repo.LowerName),	}	if err = notifyWatchers(e, action); err != nil {		return fmt.Errorf("notify watchers '%d/%s'", actUser.Id, repo.Id)	}	// Remove watch for organization.	if repo.Owner.IsOrganization() {		if err = watchRepo(e, repo.Owner.Id, repo.Id, false); err != nil {			return fmt.Errorf("watch repository: %v", err)		}	}	log.Trace("action.TransferRepoAction: %s/%s", actUser.Name, repo.Name)	return nil}// TransferRepoAction adds new action for transferring repository.func TransferRepoAction(actUser, oldOwner, newOwner *User, repo *Repository) (err error) {	return transferRepoAction(x, actUser, oldOwner, newOwner, repo)}// GetFeeds returns action list of given user in given context.func GetFeeds(uid, offset int64, isProfile bool) ([]*Action, error) {	actions := make([]*Action, 0, 20)	sess := x.Limit(20, int(offset)).Desc("id").Where("user_id=?", uid)	if isProfile {		sess.And("is_private=?", false).And("act_user_id=?", uid)	}	err := sess.Find(&actions)	return actions, err}
 |