You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

action.go 17 kB

11 years ago
11 years ago
11 years ago
10 years ago
10 years ago
11 years ago
11 years ago
11 years ago
11 years ago
10 years ago
10 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
10 years ago
10 years ago
11 years ago
10 years ago
10 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import (
  6. "encoding/json"
  7. "fmt"
  8. "path"
  9. "regexp"
  10. "strings"
  11. "time"
  12. "unicode"
  13. "github.com/Unknwon/com"
  14. "github.com/go-xorm/xorm"
  15. "github.com/gogits/git-module"
  16. api "github.com/gogits/go-gogs-client"
  17. "github.com/go-gitea/gitea/modules/base"
  18. "github.com/go-gitea/gitea/modules/log"
  19. "github.com/go-gitea/gitea/modules/setting"
  20. )
  21. type ActionType int
  22. const (
  23. ACTION_CREATE_REPO ActionType = iota + 1 // 1
  24. ACTION_RENAME_REPO // 2
  25. ACTION_STAR_REPO // 3
  26. ACTION_WATCH_REPO // 4
  27. ACTION_COMMIT_REPO // 5
  28. ACTION_CREATE_ISSUE // 6
  29. ACTION_CREATE_PULL_REQUEST // 7
  30. ACTION_TRANSFER_REPO // 8
  31. ACTION_PUSH_TAG // 9
  32. ACTION_COMMENT_ISSUE // 10
  33. ACTION_MERGE_PULL_REQUEST // 11
  34. ACTION_CLOSE_ISSUE // 12
  35. ACTION_REOPEN_ISSUE // 13
  36. ACTION_CLOSE_PULL_REQUEST // 14
  37. ACTION_REOPEN_PULL_REQUEST // 15
  38. )
  39. var (
  40. // Same as Github. See https://help.github.com/articles/closing-issues-via-commit-messages
  41. IssueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
  42. IssueReopenKeywords = []string{"reopen", "reopens", "reopened"}
  43. IssueCloseKeywordsPat, IssueReopenKeywordsPat *regexp.Regexp
  44. IssueReferenceKeywordsPat *regexp.Regexp
  45. )
  46. func assembleKeywordsPattern(words []string) string {
  47. return fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(words, "|"))
  48. }
  49. func init() {
  50. IssueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(IssueCloseKeywords))
  51. IssueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(IssueReopenKeywords))
  52. IssueReferenceKeywordsPat = regexp.MustCompile(`(?i)(?:)(^| )\S+`)
  53. }
  54. // Action represents user operation type and other information to repository.,
  55. // it implemented interface base.Actioner so that can be used in template render.
  56. type Action struct {
  57. ID int64 `xorm:"pk autoincr"`
  58. UserID int64 // Receiver user id.
  59. OpType ActionType
  60. ActUserID int64 // Action user id.
  61. ActUserName string // Action user name.
  62. ActAvatar string `xorm:"-"`
  63. RepoID int64
  64. RepoUserName string
  65. RepoName string
  66. RefName string
  67. IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
  68. Content string `xorm:"TEXT"`
  69. Created time.Time `xorm:"-"`
  70. CreatedUnix int64
  71. }
  72. func (a *Action) BeforeInsert() {
  73. a.CreatedUnix = time.Now().Unix()
  74. }
  75. func (a *Action) AfterSet(colName string, _ xorm.Cell) {
  76. switch colName {
  77. case "created_unix":
  78. a.Created = time.Unix(a.CreatedUnix, 0).Local()
  79. }
  80. }
  81. func (a *Action) GetOpType() int {
  82. return int(a.OpType)
  83. }
  84. func (a *Action) GetActUserName() string {
  85. return a.ActUserName
  86. }
  87. func (a *Action) ShortActUserName() string {
  88. return base.EllipsisString(a.ActUserName, 20)
  89. }
  90. func (a *Action) GetRepoUserName() string {
  91. return a.RepoUserName
  92. }
  93. func (a *Action) ShortRepoUserName() string {
  94. return base.EllipsisString(a.RepoUserName, 20)
  95. }
  96. func (a *Action) GetRepoName() string {
  97. return a.RepoName
  98. }
  99. func (a *Action) ShortRepoName() string {
  100. return base.EllipsisString(a.RepoName, 33)
  101. }
  102. func (a *Action) GetRepoPath() string {
  103. return path.Join(a.RepoUserName, a.RepoName)
  104. }
  105. func (a *Action) ShortRepoPath() string {
  106. return path.Join(a.ShortRepoUserName(), a.ShortRepoName())
  107. }
  108. func (a *Action) GetRepoLink() string {
  109. if len(setting.AppSubUrl) > 0 {
  110. return path.Join(setting.AppSubUrl, a.GetRepoPath())
  111. }
  112. return "/" + a.GetRepoPath()
  113. }
  114. func (a *Action) GetBranch() string {
  115. return a.RefName
  116. }
  117. func (a *Action) GetContent() string {
  118. return a.Content
  119. }
  120. func (a *Action) GetCreate() time.Time {
  121. return a.Created
  122. }
  123. func (a *Action) GetIssueInfos() []string {
  124. return strings.SplitN(a.Content, "|", 2)
  125. }
  126. func (a *Action) GetIssueTitle() string {
  127. index := com.StrTo(a.GetIssueInfos()[0]).MustInt64()
  128. issue, err := GetIssueByIndex(a.RepoID, index)
  129. if err != nil {
  130. log.Error(4, "GetIssueByIndex: %v", err)
  131. return "500 when get issue"
  132. }
  133. return issue.Title
  134. }
  135. func (a *Action) GetIssueContent() string {
  136. index := com.StrTo(a.GetIssueInfos()[0]).MustInt64()
  137. issue, err := GetIssueByIndex(a.RepoID, index)
  138. if err != nil {
  139. log.Error(4, "GetIssueByIndex: %v", err)
  140. return "500 when get issue"
  141. }
  142. return issue.Content
  143. }
  144. func newRepoAction(e Engine, u *User, repo *Repository) (err error) {
  145. if err = notifyWatchers(e, &Action{
  146. ActUserID: u.ID,
  147. ActUserName: u.Name,
  148. OpType: ACTION_CREATE_REPO,
  149. RepoID: repo.ID,
  150. RepoUserName: repo.Owner.Name,
  151. RepoName: repo.Name,
  152. IsPrivate: repo.IsPrivate,
  153. }); err != nil {
  154. return fmt.Errorf("notify watchers '%d/%d': %v", u.ID, repo.ID, err)
  155. }
  156. log.Trace("action.newRepoAction: %s/%s", u.Name, repo.Name)
  157. return err
  158. }
  159. // NewRepoAction adds new action for creating repository.
  160. func NewRepoAction(u *User, repo *Repository) (err error) {
  161. return newRepoAction(x, u, repo)
  162. }
  163. func renameRepoAction(e Engine, actUser *User, oldRepoName string, repo *Repository) (err error) {
  164. if err = notifyWatchers(e, &Action{
  165. ActUserID: actUser.ID,
  166. ActUserName: actUser.Name,
  167. OpType: ACTION_RENAME_REPO,
  168. RepoID: repo.ID,
  169. RepoUserName: repo.Owner.Name,
  170. RepoName: repo.Name,
  171. IsPrivate: repo.IsPrivate,
  172. Content: oldRepoName,
  173. }); err != nil {
  174. return fmt.Errorf("notify watchers: %v", err)
  175. }
  176. log.Trace("action.renameRepoAction: %s/%s", actUser.Name, repo.Name)
  177. return nil
  178. }
  179. // RenameRepoAction adds new action for renaming a repository.
  180. func RenameRepoAction(actUser *User, oldRepoName string, repo *Repository) error {
  181. return renameRepoAction(x, actUser, oldRepoName, repo)
  182. }
  183. func issueIndexTrimRight(c rune) bool {
  184. return !unicode.IsDigit(c)
  185. }
  186. type PushCommit struct {
  187. Sha1 string
  188. Message string
  189. AuthorEmail string
  190. AuthorName string
  191. CommitterEmail string
  192. CommitterName string
  193. Timestamp time.Time
  194. }
  195. type PushCommits struct {
  196. Len int
  197. Commits []*PushCommit
  198. CompareURL string
  199. avatars map[string]string
  200. }
  201. func NewPushCommits() *PushCommits {
  202. return &PushCommits{
  203. avatars: make(map[string]string),
  204. }
  205. }
  206. func (pc *PushCommits) ToApiPayloadCommits(repoLink string) []*api.PayloadCommit {
  207. commits := make([]*api.PayloadCommit, len(pc.Commits))
  208. for i, commit := range pc.Commits {
  209. authorUsername := ""
  210. author, err := GetUserByEmail(commit.AuthorEmail)
  211. if err == nil {
  212. authorUsername = author.Name
  213. }
  214. committerUsername := ""
  215. committer, err := GetUserByEmail(commit.CommitterEmail)
  216. if err == nil {
  217. // TODO: check errors other than email not found.
  218. committerUsername = committer.Name
  219. }
  220. commits[i] = &api.PayloadCommit{
  221. ID: commit.Sha1,
  222. Message: commit.Message,
  223. URL: fmt.Sprintf("%s/commit/%s", repoLink, commit.Sha1),
  224. Author: &api.PayloadUser{
  225. Name: commit.AuthorName,
  226. Email: commit.AuthorEmail,
  227. UserName: authorUsername,
  228. },
  229. Committer: &api.PayloadUser{
  230. Name: commit.CommitterName,
  231. Email: commit.CommitterEmail,
  232. UserName: committerUsername,
  233. },
  234. Timestamp: commit.Timestamp,
  235. }
  236. }
  237. return commits
  238. }
  239. // AvatarLink tries to match user in database with e-mail
  240. // in order to show custom avatar, and falls back to general avatar link.
  241. func (push *PushCommits) AvatarLink(email string) string {
  242. _, ok := push.avatars[email]
  243. if !ok {
  244. u, err := GetUserByEmail(email)
  245. if err != nil {
  246. push.avatars[email] = base.AvatarLink(email)
  247. if !IsErrUserNotExist(err) {
  248. log.Error(4, "GetUserByEmail: %v", err)
  249. }
  250. } else {
  251. push.avatars[email] = u.RelAvatarLink()
  252. }
  253. }
  254. return push.avatars[email]
  255. }
  256. // UpdateIssuesCommit checks if issues are manipulated by commit message.
  257. func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit) error {
  258. // Commits are appended in the reverse order.
  259. for i := len(commits) - 1; i >= 0; i-- {
  260. c := commits[i]
  261. refMarked := make(map[int64]bool)
  262. for _, ref := range IssueReferenceKeywordsPat.FindAllString(c.Message, -1) {
  263. ref = ref[strings.IndexByte(ref, byte(' '))+1:]
  264. ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
  265. if len(ref) == 0 {
  266. continue
  267. }
  268. // Add repo name if missing
  269. if ref[0] == '#' {
  270. ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
  271. } else if !strings.Contains(ref, "/") {
  272. // FIXME: We don't support User#ID syntax yet
  273. // return ErrNotImplemented
  274. continue
  275. }
  276. issue, err := GetIssueByRef(ref)
  277. if err != nil {
  278. if IsErrIssueNotExist(err) {
  279. continue
  280. }
  281. return err
  282. }
  283. if refMarked[issue.ID] {
  284. continue
  285. }
  286. refMarked[issue.ID] = true
  287. message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, c.Message)
  288. if err = CreateRefComment(doer, repo, issue, message, c.Sha1); err != nil {
  289. return err
  290. }
  291. }
  292. refMarked = make(map[int64]bool)
  293. // FIXME: can merge this one and next one to a common function.
  294. for _, ref := range IssueCloseKeywordsPat.FindAllString(c.Message, -1) {
  295. ref = ref[strings.IndexByte(ref, byte(' '))+1:]
  296. ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
  297. if len(ref) == 0 {
  298. continue
  299. }
  300. // Add repo name if missing
  301. if ref[0] == '#' {
  302. ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
  303. } else if !strings.Contains(ref, "/") {
  304. // We don't support User#ID syntax yet
  305. // return ErrNotImplemented
  306. continue
  307. }
  308. issue, err := GetIssueByRef(ref)
  309. if err != nil {
  310. if IsErrIssueNotExist(err) {
  311. continue
  312. }
  313. return err
  314. }
  315. if refMarked[issue.ID] {
  316. continue
  317. }
  318. refMarked[issue.ID] = true
  319. if issue.RepoID != repo.ID || issue.IsClosed {
  320. continue
  321. }
  322. if err = issue.ChangeStatus(doer, repo, true); err != nil {
  323. return err
  324. }
  325. }
  326. // It is conflict to have close and reopen at same time, so refsMarkd doesn't need to reinit here.
  327. for _, ref := range IssueReopenKeywordsPat.FindAllString(c.Message, -1) {
  328. ref = ref[strings.IndexByte(ref, byte(' '))+1:]
  329. ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
  330. if len(ref) == 0 {
  331. continue
  332. }
  333. // Add repo name if missing
  334. if ref[0] == '#' {
  335. ref = fmt.Sprintf("%s%s", repo.FullName(), ref)
  336. } else if !strings.Contains(ref, "/") {
  337. // We don't support User#ID syntax yet
  338. // return ErrNotImplemented
  339. continue
  340. }
  341. issue, err := GetIssueByRef(ref)
  342. if err != nil {
  343. if IsErrIssueNotExist(err) {
  344. continue
  345. }
  346. return err
  347. }
  348. if refMarked[issue.ID] {
  349. continue
  350. }
  351. refMarked[issue.ID] = true
  352. if issue.RepoID != repo.ID || !issue.IsClosed {
  353. continue
  354. }
  355. if err = issue.ChangeStatus(doer, repo, false); err != nil {
  356. return err
  357. }
  358. }
  359. }
  360. return nil
  361. }
  362. type CommitRepoActionOptions struct {
  363. PusherName string
  364. RepoOwnerID int64
  365. RepoName string
  366. RefFullName string
  367. OldCommitID string
  368. NewCommitID string
  369. Commits *PushCommits
  370. }
  371. // CommitRepoAction adds new commit actio to the repository, and prepare corresponding webhooks.
  372. func CommitRepoAction(opts CommitRepoActionOptions) error {
  373. pusher, err := GetUserByName(opts.PusherName)
  374. if err != nil {
  375. return fmt.Errorf("GetUserByName [%s]: %v", opts.PusherName, err)
  376. }
  377. repo, err := GetRepositoryByName(opts.RepoOwnerID, opts.RepoName)
  378. if err != nil {
  379. return fmt.Errorf("GetRepositoryByName [owner_id: %d, name: %s]: %v", opts.RepoOwnerID, opts.RepoName, err)
  380. }
  381. // Change repository bare status and update last updated time.
  382. repo.IsBare = false
  383. if err = UpdateRepository(repo, false); err != nil {
  384. return fmt.Errorf("UpdateRepository: %v", err)
  385. }
  386. isNewBranch := false
  387. opType := ACTION_COMMIT_REPO
  388. // Check it's tag push or branch.
  389. if strings.HasPrefix(opts.RefFullName, git.TAG_PREFIX) {
  390. opType = ACTION_PUSH_TAG
  391. opts.Commits = &PushCommits{}
  392. } else {
  393. // if not the first commit, set the compare URL.
  394. if opts.OldCommitID == git.EMPTY_SHA {
  395. isNewBranch = true
  396. } else {
  397. opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
  398. }
  399. if err = UpdateIssuesCommit(pusher, repo, opts.Commits.Commits); err != nil {
  400. log.Error(4, "updateIssuesCommit: %v", err)
  401. }
  402. }
  403. if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum {
  404. opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum]
  405. }
  406. data, err := json.Marshal(opts.Commits)
  407. if err != nil {
  408. return fmt.Errorf("Marshal: %v", err)
  409. }
  410. refName := git.RefEndName(opts.RefFullName)
  411. if err = NotifyWatchers(&Action{
  412. ActUserID: pusher.ID,
  413. ActUserName: pusher.Name,
  414. OpType: opType,
  415. Content: string(data),
  416. RepoID: repo.ID,
  417. RepoUserName: repo.MustOwner().Name,
  418. RepoName: repo.Name,
  419. RefName: refName,
  420. IsPrivate: repo.IsPrivate,
  421. }); err != nil {
  422. return fmt.Errorf("NotifyWatchers: %v", err)
  423. }
  424. defer func() {
  425. go HookQueue.Add(repo.ID)
  426. }()
  427. apiPusher := pusher.APIFormat()
  428. apiRepo := repo.APIFormat(nil)
  429. switch opType {
  430. case ACTION_COMMIT_REPO: // Push
  431. if err = PrepareWebhooks(repo, HOOK_EVENT_PUSH, &api.PushPayload{
  432. Ref: opts.RefFullName,
  433. Before: opts.OldCommitID,
  434. After: opts.NewCommitID,
  435. CompareURL: setting.AppUrl + opts.Commits.CompareURL,
  436. Commits: opts.Commits.ToApiPayloadCommits(repo.HTMLURL()),
  437. Repo: apiRepo,
  438. Pusher: apiPusher,
  439. Sender: apiPusher,
  440. }); err != nil {
  441. return fmt.Errorf("PrepareWebhooks: %v", err)
  442. }
  443. if isNewBranch {
  444. return PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
  445. Ref: refName,
  446. RefType: "branch",
  447. Repo: apiRepo,
  448. Sender: apiPusher,
  449. })
  450. }
  451. case ACTION_PUSH_TAG: // Create
  452. return PrepareWebhooks(repo, HOOK_EVENT_CREATE, &api.CreatePayload{
  453. Ref: refName,
  454. RefType: "tag",
  455. Repo: apiRepo,
  456. Sender: apiPusher,
  457. })
  458. }
  459. return nil
  460. }
  461. func transferRepoAction(e Engine, doer, oldOwner *User, repo *Repository) (err error) {
  462. if err = notifyWatchers(e, &Action{
  463. ActUserID: doer.ID,
  464. ActUserName: doer.Name,
  465. OpType: ACTION_TRANSFER_REPO,
  466. RepoID: repo.ID,
  467. RepoUserName: repo.Owner.Name,
  468. RepoName: repo.Name,
  469. IsPrivate: repo.IsPrivate,
  470. Content: path.Join(oldOwner.Name, repo.Name),
  471. }); err != nil {
  472. return fmt.Errorf("notifyWatchers: %v", err)
  473. }
  474. // Remove watch for organization.
  475. if oldOwner.IsOrganization() {
  476. if err = watchRepo(e, oldOwner.ID, repo.ID, false); err != nil {
  477. return fmt.Errorf("watchRepo [false]: %v", err)
  478. }
  479. }
  480. return nil
  481. }
  482. // TransferRepoAction adds new action for transferring repository,
  483. // the Owner field of repository is assumed to be new owner.
  484. func TransferRepoAction(doer, oldOwner *User, repo *Repository) error {
  485. return transferRepoAction(x, doer, oldOwner, repo)
  486. }
  487. func mergePullRequestAction(e Engine, doer *User, repo *Repository, issue *Issue) error {
  488. return notifyWatchers(e, &Action{
  489. ActUserID: doer.ID,
  490. ActUserName: doer.Name,
  491. OpType: ACTION_MERGE_PULL_REQUEST,
  492. Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
  493. RepoID: repo.ID,
  494. RepoUserName: repo.Owner.Name,
  495. RepoName: repo.Name,
  496. IsPrivate: repo.IsPrivate,
  497. })
  498. }
  499. // MergePullRequestAction adds new action for merging pull request.
  500. func MergePullRequestAction(actUser *User, repo *Repository, pull *Issue) error {
  501. return mergePullRequestAction(x, actUser, repo, pull)
  502. }
  503. // GetFeeds returns action list of given user in given context.
  504. // actorID is the user who's requesting, ctxUserID is the user/org that is requested.
  505. // actorID can be -1 when isProfile is true or to skip the permission check.
  506. func GetFeeds(ctxUser *User, actorID, offset int64, isProfile bool) ([]*Action, error) {
  507. actions := make([]*Action, 0, 20)
  508. sess := x.Limit(20, int(offset)).Desc("id").Where("user_id = ?", ctxUser.ID)
  509. if isProfile {
  510. sess.And("is_private = ?", false).And("act_user_id = ?", ctxUser.ID)
  511. } else if actorID != -1 && ctxUser.IsOrganization() {
  512. // FIXME: only need to get IDs here, not all fields of repository.
  513. repos, _, err := ctxUser.GetUserRepositories(actorID, 1, ctxUser.NumRepos)
  514. if err != nil {
  515. return nil, fmt.Errorf("GetUserRepositories: %v", err)
  516. }
  517. var repoIDs []int64
  518. for _, repo := range repos {
  519. repoIDs = append(repoIDs, repo.ID)
  520. }
  521. if len(repoIDs) > 0 {
  522. sess.In("repo_id", repoIDs)
  523. }
  524. }
  525. err := sess.Find(&actions)
  526. return actions, err
  527. }