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 14 kB

11 years ago
11 years ago
11 years ago
10 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
10 years ago
10 years ago
10 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  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. "errors"
  8. "fmt"
  9. "path"
  10. "regexp"
  11. "strings"
  12. "time"
  13. "unicode"
  14. "github.com/go-xorm/xorm"
  15. "github.com/gogits/gogs/modules/base"
  16. "github.com/gogits/gogs/modules/git"
  17. "github.com/gogits/gogs/modules/log"
  18. "github.com/gogits/gogs/modules/setting"
  19. )
  20. type ActionType int
  21. const (
  22. CREATE_REPO ActionType = iota + 1 // 1
  23. DELETE_REPO // 2
  24. STAR_REPO // 3
  25. FOLLOW_REPO // 4
  26. COMMIT_REPO // 5
  27. CREATE_ISSUE // 6
  28. PULL_REQUEST // 7
  29. TRANSFER_REPO // 8
  30. PUSH_TAG // 9
  31. COMMENT_ISSUE // 10
  32. )
  33. var (
  34. ErrNotImplemented = errors.New("Not implemented yet")
  35. )
  36. var (
  37. // Same as Github. See https://help.github.com/articles/closing-issues-via-commit-messages
  38. IssueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
  39. IssueReopenKeywords = []string{"reopen", "reopens", "reopened"}
  40. IssueCloseKeywordsPat, IssueReopenKeywordsPat *regexp.Regexp
  41. IssueReferenceKeywordsPat *regexp.Regexp
  42. )
  43. func assembleKeywordsPattern(words []string) string {
  44. return fmt.Sprintf(`(?i)(?:%s) \S+`, strings.Join(words, "|"))
  45. }
  46. func init() {
  47. IssueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(IssueCloseKeywords))
  48. IssueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(IssueReopenKeywords))
  49. IssueReferenceKeywordsPat = regexp.MustCompile(`(?i)(?:)(^| )\S+`)
  50. }
  51. // Action represents user operation type and other information to repository.,
  52. // it implemented interface base.Actioner so that can be used in template render.
  53. type Action struct {
  54. ID int64 `xorm:"pk autoincr"`
  55. UserID int64 // Receiver user id.
  56. OpType ActionType
  57. ActUserID int64 // Action user id.
  58. ActUserName string // Action user name.
  59. ActEmail string
  60. ActAvatar string `xorm:"-"`
  61. RepoID int64
  62. RepoUserName string
  63. RepoName string
  64. RefName string
  65. IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
  66. Content string `xorm:"TEXT"`
  67. Created time.Time `xorm:"created"`
  68. }
  69. func (a *Action) AfterSet(colName string, _ xorm.Cell) {
  70. switch colName {
  71. case "created":
  72. a.Created = regulateTimeZone(a.Created)
  73. }
  74. }
  75. func (a Action) GetOpType() int {
  76. return int(a.OpType)
  77. }
  78. func (a Action) GetActUserName() string {
  79. return a.ActUserName
  80. }
  81. func (a Action) GetActEmail() string {
  82. return a.ActEmail
  83. }
  84. func (a Action) GetRepoUserName() string {
  85. return a.RepoUserName
  86. }
  87. func (a Action) GetRepoName() string {
  88. return a.RepoName
  89. }
  90. func (a Action) GetRepoPath() string {
  91. return path.Join(a.RepoUserName, a.RepoName)
  92. }
  93. func (a Action) GetRepoLink() string {
  94. if len(setting.AppSubUrl) > 0 {
  95. return path.Join(setting.AppSubUrl, a.GetRepoPath())
  96. }
  97. return "/" + a.GetRepoPath()
  98. }
  99. func (a Action) GetBranch() string {
  100. return a.RefName
  101. }
  102. func (a Action) GetContent() string {
  103. return a.Content
  104. }
  105. func (a Action) GetCreate() time.Time {
  106. return a.Created
  107. }
  108. func (a Action) GetIssueInfos() []string {
  109. return strings.SplitN(a.Content, "|", 2)
  110. }
  111. // updateIssuesCommit checks if issues are manipulated by commit message.
  112. func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string, commits []*base.PushCommit) error {
  113. for _, c := range commits {
  114. for _, ref := range IssueReferenceKeywordsPat.FindAllString(c.Message, -1) {
  115. ref := ref[strings.IndexByte(ref, byte(' '))+1:]
  116. ref = strings.TrimRightFunc(ref, func(c rune) bool {
  117. return !unicode.IsDigit(c)
  118. })
  119. if len(ref) == 0 {
  120. continue
  121. }
  122. // Add repo name if missing
  123. if ref[0] == '#' {
  124. ref = fmt.Sprintf("%s/%s%s", repoUserName, repoName, ref)
  125. } else if strings.Contains(ref, "/") == false {
  126. // FIXME: We don't support User#ID syntax yet
  127. // return ErrNotImplemented
  128. continue
  129. }
  130. issue, err := GetIssueByRef(ref)
  131. if err != nil {
  132. return err
  133. }
  134. url := fmt.Sprintf("%s/%s/%s/commit/%s", setting.AppSubUrl, repoUserName, repoName, c.Sha1)
  135. message := fmt.Sprintf(`<a href="%s">%s</a>`, url, c.Message)
  136. if _, err = CreateComment(u, repo, issue, 0, 0, COMMENT_TYPE_COMMIT_REF, message, nil); err != nil {
  137. return err
  138. }
  139. }
  140. for _, ref := range IssueCloseKeywordsPat.FindAllString(c.Message, -1) {
  141. ref := ref[strings.IndexByte(ref, byte(' '))+1:]
  142. ref = strings.TrimRightFunc(ref, func(c rune) bool {
  143. return !unicode.IsDigit(c)
  144. })
  145. if len(ref) == 0 {
  146. continue
  147. }
  148. // Add repo name if missing
  149. if ref[0] == '#' {
  150. ref = fmt.Sprintf("%s/%s%s", repoUserName, repoName, ref)
  151. } else if strings.Contains(ref, "/") == false {
  152. // We don't support User#ID syntax yet
  153. // return ErrNotImplemented
  154. continue
  155. }
  156. issue, err := GetIssueByRef(ref)
  157. if err != nil {
  158. return err
  159. }
  160. if issue.RepoID == repo.ID {
  161. if issue.IsClosed {
  162. continue
  163. }
  164. issue.IsClosed = true
  165. if err = issue.GetLabels(); err != nil {
  166. return err
  167. }
  168. for _, label := range issue.Labels {
  169. label.NumClosedIssues++
  170. if err = UpdateLabel(label); err != nil {
  171. return err
  172. }
  173. }
  174. if err = UpdateIssue(issue); err != nil {
  175. return err
  176. } else if err = UpdateIssueUsersByStatus(issue.ID, issue.IsClosed); err != nil {
  177. return err
  178. }
  179. if err = ChangeMilestoneIssueStats(issue); err != nil {
  180. return err
  181. }
  182. // If commit happened in the referenced repository, it means the issue can be closed.
  183. if _, err = CreateComment(u, repo, issue, 0, 0, COMMENT_TYPE_CLOSE, "", nil); err != nil {
  184. return err
  185. }
  186. }
  187. }
  188. for _, ref := range IssueReopenKeywordsPat.FindAllString(c.Message, -1) {
  189. ref := ref[strings.IndexByte(ref, byte(' '))+1:]
  190. ref = strings.TrimRightFunc(ref, func(c rune) bool {
  191. return !unicode.IsDigit(c)
  192. })
  193. if len(ref) == 0 {
  194. continue
  195. }
  196. // Add repo name if missing
  197. if ref[0] == '#' {
  198. ref = fmt.Sprintf("%s/%s%s", repoUserName, repoName, ref)
  199. } else if strings.Contains(ref, "/") == false {
  200. // We don't support User#ID syntax yet
  201. // return ErrNotImplemented
  202. continue
  203. }
  204. issue, err := GetIssueByRef(ref)
  205. if err != nil {
  206. return err
  207. }
  208. if issue.RepoID == repo.ID {
  209. if !issue.IsClosed {
  210. continue
  211. }
  212. issue.IsClosed = false
  213. if err = issue.GetLabels(); err != nil {
  214. return err
  215. }
  216. for _, label := range issue.Labels {
  217. label.NumClosedIssues--
  218. if err = UpdateLabel(label); err != nil {
  219. return err
  220. }
  221. }
  222. if err = UpdateIssue(issue); err != nil {
  223. return err
  224. } else if err = UpdateIssueUsersByStatus(issue.ID, issue.IsClosed); err != nil {
  225. return err
  226. }
  227. if err = ChangeMilestoneIssueStats(issue); err != nil {
  228. return err
  229. }
  230. // If commit happened in the referenced repository, it means the issue can be closed.
  231. if _, err = CreateComment(u, repo, issue, 0, 0, COMMENT_TYPE_REOPEN, "", nil); err != nil {
  232. return err
  233. }
  234. }
  235. }
  236. }
  237. return nil
  238. }
  239. // CommitRepoAction adds new action for committing repository.
  240. func CommitRepoAction(userID, repoUserID int64, userName, actEmail string,
  241. repoID int64, repoUserName, repoName string, refFullName string, commit *base.PushCommits, oldCommitID string, newCommitID string) error {
  242. opType := COMMIT_REPO
  243. // Check it's tag push or branch.
  244. if strings.HasPrefix(refFullName, "refs/tags/") {
  245. opType = PUSH_TAG
  246. commit = &base.PushCommits{}
  247. }
  248. repoLink := fmt.Sprintf("%s%s/%s", setting.AppUrl, repoUserName, repoName)
  249. // if not the first commit, set the compareUrl
  250. if !strings.HasPrefix(oldCommitID, "0000000") {
  251. commit.CompareUrl = fmt.Sprintf("%s/%s/compare/%s...%s", repoUserName, repoName, oldCommitID, newCommitID)
  252. }
  253. bs, err := json.Marshal(commit)
  254. if err != nil {
  255. return fmt.Errorf("Marshal: %v", err)
  256. }
  257. refName := git.RefEndName(refFullName)
  258. // Change repository bare status and update last updated time.
  259. repo, err := GetRepositoryByName(repoUserID, repoName)
  260. if err != nil {
  261. return fmt.Errorf("GetRepositoryByName: %v", err)
  262. }
  263. repo.IsBare = false
  264. if err = UpdateRepository(repo, false); err != nil {
  265. return fmt.Errorf("UpdateRepository: %v", err)
  266. }
  267. u, err := GetUserByID(userID)
  268. if err != nil {
  269. return fmt.Errorf("GetUserByID: %v", err)
  270. }
  271. err = updateIssuesCommit(u, repo, repoUserName, repoName, commit.Commits)
  272. if err != nil {
  273. log.Debug("updateIssuesCommit: ", err)
  274. }
  275. if err = NotifyWatchers(&Action{
  276. ActUserID: u.Id,
  277. ActUserName: userName,
  278. ActEmail: actEmail,
  279. OpType: opType,
  280. Content: string(bs),
  281. RepoID: repo.ID,
  282. RepoUserName: repoUserName,
  283. RepoName: repoName,
  284. RefName: refName,
  285. IsPrivate: repo.IsPrivate,
  286. }); err != nil {
  287. return errors.New("NotifyWatchers: " + err.Error())
  288. }
  289. // New push event hook.
  290. if err := repo.GetOwner(); err != nil {
  291. return errors.New("GetOwner: " + err.Error())
  292. }
  293. ws, err := GetActiveWebhooksByRepoId(repo.ID)
  294. if err != nil {
  295. return errors.New("GetActiveWebhooksByRepoId: " + err.Error())
  296. }
  297. // check if repo belongs to org and append additional webhooks
  298. if repo.Owner.IsOrganization() {
  299. // get hooks for org
  300. orgws, err := GetActiveWebhooksByOrgId(repo.OwnerID)
  301. if err != nil {
  302. return errors.New("GetActiveWebhooksByOrgId: " + err.Error())
  303. }
  304. ws = append(ws, orgws...)
  305. }
  306. if len(ws) == 0 {
  307. return nil
  308. }
  309. pusher_email, pusher_name := "", ""
  310. pusher, err := GetUserByName(userName)
  311. if err == nil {
  312. pusher_email = pusher.Email
  313. pusher_name = pusher.DisplayName()
  314. }
  315. commits := make([]*PayloadCommit, len(commit.Commits))
  316. for i, cmt := range commit.Commits {
  317. author_username := ""
  318. author, err := GetUserByEmail(cmt.AuthorEmail)
  319. if err == nil {
  320. author_username = author.Name
  321. }
  322. commits[i] = &PayloadCommit{
  323. Id: cmt.Sha1,
  324. Message: cmt.Message,
  325. Url: fmt.Sprintf("%s/commit/%s", repoLink, cmt.Sha1),
  326. Author: &PayloadAuthor{
  327. Name: cmt.AuthorName,
  328. Email: cmt.AuthorEmail,
  329. UserName: author_username,
  330. },
  331. }
  332. }
  333. p := &Payload{
  334. Ref: refFullName,
  335. Commits: commits,
  336. Repo: &PayloadRepo{
  337. Id: repo.ID,
  338. Name: repo.LowerName,
  339. Url: repoLink,
  340. Description: repo.Description,
  341. Website: repo.Website,
  342. Watchers: repo.NumWatches,
  343. Owner: &PayloadAuthor{
  344. Name: repo.Owner.DisplayName(),
  345. Email: repo.Owner.Email,
  346. UserName: repo.Owner.Name,
  347. },
  348. Private: repo.IsPrivate,
  349. },
  350. Pusher: &PayloadAuthor{
  351. Name: pusher_name,
  352. Email: pusher_email,
  353. UserName: userName,
  354. },
  355. Before: oldCommitID,
  356. After: newCommitID,
  357. CompareUrl: setting.AppUrl + commit.CompareUrl,
  358. }
  359. for _, w := range ws {
  360. w.GetEvent()
  361. if !w.HasPushEvent() {
  362. continue
  363. }
  364. var payload BasePayload
  365. switch w.HookTaskType {
  366. case SLACK:
  367. s, err := GetSlackPayload(p, w.Meta)
  368. if err != nil {
  369. return errors.New("action.GetSlackPayload: " + err.Error())
  370. }
  371. payload = s
  372. default:
  373. payload = p
  374. p.Secret = w.Secret
  375. }
  376. if err = CreateHookTask(&HookTask{
  377. RepoID: repo.ID,
  378. HookID: w.ID,
  379. Type: w.HookTaskType,
  380. URL: w.URL,
  381. BasePayload: payload,
  382. ContentType: w.ContentType,
  383. EventType: HOOK_EVENT_PUSH,
  384. IsSSL: w.IsSSL,
  385. }); err != nil {
  386. return fmt.Errorf("CreateHookTask: %v", err)
  387. }
  388. }
  389. return nil
  390. }
  391. func newRepoAction(e Engine, u *User, repo *Repository) (err error) {
  392. if err = notifyWatchers(e, &Action{
  393. ActUserID: u.Id,
  394. ActUserName: u.Name,
  395. ActEmail: u.Email,
  396. OpType: CREATE_REPO,
  397. RepoID: repo.ID,
  398. RepoUserName: repo.Owner.Name,
  399. RepoName: repo.Name,
  400. IsPrivate: repo.IsPrivate,
  401. }); err != nil {
  402. return fmt.Errorf("notify watchers '%d/%s'", u.Id, repo.ID)
  403. }
  404. log.Trace("action.NewRepoAction: %s/%s", u.Name, repo.Name)
  405. return err
  406. }
  407. // NewRepoAction adds new action for creating repository.
  408. func NewRepoAction(u *User, repo *Repository) (err error) {
  409. return newRepoAction(x, u, repo)
  410. }
  411. func transferRepoAction(e Engine, actUser, oldOwner, newOwner *User, repo *Repository) (err error) {
  412. action := &Action{
  413. ActUserID: actUser.Id,
  414. ActUserName: actUser.Name,
  415. ActEmail: actUser.Email,
  416. OpType: TRANSFER_REPO,
  417. RepoID: repo.ID,
  418. RepoUserName: newOwner.Name,
  419. RepoName: repo.Name,
  420. IsPrivate: repo.IsPrivate,
  421. Content: path.Join(oldOwner.LowerName, repo.LowerName),
  422. }
  423. if err = notifyWatchers(e, action); err != nil {
  424. return fmt.Errorf("notify watchers '%d/%s'", actUser.Id, repo.ID)
  425. }
  426. // Remove watch for organization.
  427. if repo.Owner.IsOrganization() {
  428. if err = watchRepo(e, repo.Owner.Id, repo.ID, false); err != nil {
  429. return fmt.Errorf("watch repository: %v", err)
  430. }
  431. }
  432. log.Trace("action.TransferRepoAction: %s/%s", actUser.Name, repo.Name)
  433. return nil
  434. }
  435. // TransferRepoAction adds new action for transferring repository.
  436. func TransferRepoAction(actUser, oldOwner, newOwner *User, repo *Repository) (err error) {
  437. return transferRepoAction(x, actUser, oldOwner, newOwner, repo)
  438. }
  439. // GetFeeds returns action list of given user in given context.
  440. func GetFeeds(uid, offset int64, isProfile bool) ([]*Action, error) {
  441. actions := make([]*Action, 0, 20)
  442. sess := x.Limit(20, int(offset)).Desc("id").Where("user_id=?", uid)
  443. if isProfile {
  444. sess.And("is_private=?", false).And("act_user_id=?", uid)
  445. }
  446. err := sess.Find(&actions)
  447. return actions, err
  448. }