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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  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. func updateIssuesCommit(u *User, repo *Repository, repoUserName, repoName string, commits []*base.PushCommit) error {
  112. for _, c := range commits {
  113. for _, ref := range IssueReferenceKeywordsPat.FindAllString(c.Message, -1) {
  114. ref := ref[strings.IndexByte(ref, byte(' '))+1:]
  115. ref = strings.TrimRightFunc(ref, func(c rune) bool {
  116. return !unicode.IsDigit(c)
  117. })
  118. if len(ref) == 0 {
  119. continue
  120. }
  121. // Add repo name if missing
  122. if ref[0] == '#' {
  123. ref = fmt.Sprintf("%s/%s%s", repoUserName, repoName, ref)
  124. } else if strings.Contains(ref, "/") == false {
  125. // FIXME: We don't support User#ID syntax yet
  126. // return ErrNotImplemented
  127. continue
  128. }
  129. issue, err := GetIssueByRef(ref)
  130. if err != nil {
  131. return err
  132. }
  133. url := fmt.Sprintf("%s/%s/%s/commit/%s", setting.AppSubUrl, repoUserName, repoName, c.Sha1)
  134. message := fmt.Sprintf(`<a href="%s">%s</a>`, url, c.Message)
  135. if _, err = CreateComment(u, repo, issue, 0, 0, COMMENT_TYPE_COMMIT_REF, message, nil); err != nil {
  136. return err
  137. }
  138. }
  139. for _, ref := range IssueCloseKeywordsPat.FindAllString(c.Message, -1) {
  140. ref := ref[strings.IndexByte(ref, byte(' '))+1:]
  141. ref = strings.TrimRightFunc(ref, func(c rune) bool {
  142. return !unicode.IsDigit(c)
  143. })
  144. if len(ref) == 0 {
  145. continue
  146. }
  147. // Add repo name if missing
  148. if ref[0] == '#' {
  149. ref = fmt.Sprintf("%s/%s%s", repoUserName, repoName, ref)
  150. } else if strings.Contains(ref, "/") == false {
  151. // We don't support User#ID syntax yet
  152. // return ErrNotImplemented
  153. continue
  154. }
  155. issue, err := GetIssueByRef(ref)
  156. if err != nil {
  157. return err
  158. }
  159. if issue.RepoID == repo.ID {
  160. if issue.IsClosed {
  161. continue
  162. }
  163. issue.IsClosed = true
  164. if err = issue.GetLabels(); err != nil {
  165. return err
  166. }
  167. for _, label := range issue.Labels {
  168. label.NumClosedIssues++
  169. if err = UpdateLabel(label); err != nil {
  170. return err
  171. }
  172. }
  173. if err = UpdateIssue(issue); err != nil {
  174. return err
  175. } else if err = UpdateIssueUsersByStatus(issue.ID, issue.IsClosed); err != nil {
  176. return err
  177. }
  178. if err = ChangeMilestoneIssueStats(issue); err != nil {
  179. return err
  180. }
  181. // If commit happened in the referenced repository, it means the issue can be closed.
  182. if _, err = CreateComment(u, repo, issue, 0, 0, COMMENT_TYPE_CLOSE, "", nil); err != nil {
  183. return err
  184. }
  185. }
  186. }
  187. for _, ref := range IssueReopenKeywordsPat.FindAllString(c.Message, -1) {
  188. ref := ref[strings.IndexByte(ref, byte(' '))+1:]
  189. ref = strings.TrimRightFunc(ref, func(c rune) bool {
  190. return !unicode.IsDigit(c)
  191. })
  192. if len(ref) == 0 {
  193. continue
  194. }
  195. // Add repo name if missing
  196. if ref[0] == '#' {
  197. ref = fmt.Sprintf("%s/%s%s", repoUserName, repoName, ref)
  198. } else if strings.Contains(ref, "/") == false {
  199. // We don't support User#ID syntax yet
  200. // return ErrNotImplemented
  201. continue
  202. }
  203. issue, err := GetIssueByRef(ref)
  204. if err != nil {
  205. return err
  206. }
  207. if issue.RepoID == repo.ID {
  208. if !issue.IsClosed {
  209. continue
  210. }
  211. issue.IsClosed = false
  212. if err = issue.GetLabels(); err != nil {
  213. return err
  214. }
  215. for _, label := range issue.Labels {
  216. label.NumClosedIssues--
  217. if err = UpdateLabel(label); err != nil {
  218. return err
  219. }
  220. }
  221. if err = UpdateIssue(issue); err != nil {
  222. return err
  223. } else if err = UpdateIssueUsersByStatus(issue.ID, issue.IsClosed); err != nil {
  224. return err
  225. }
  226. if err = ChangeMilestoneIssueStats(issue); err != nil {
  227. return err
  228. }
  229. // If commit happened in the referenced repository, it means the issue can be closed.
  230. if _, err = CreateComment(u, repo, issue, 0, 0, COMMENT_TYPE_REOPEN, "", nil); err != nil {
  231. return err
  232. }
  233. }
  234. }
  235. }
  236. return nil
  237. }
  238. // CommitRepoAction adds new action for committing repository.
  239. func CommitRepoAction(userID, repoUserID int64, userName, actEmail string,
  240. repoID int64, repoUserName, repoName string, refFullName string, commit *base.PushCommits, oldCommitID string, newCommitID string) error {
  241. opType := COMMIT_REPO
  242. // Check it's tag push or branch.
  243. if strings.HasPrefix(refFullName, "refs/tags/") {
  244. opType = PUSH_TAG
  245. commit = &base.PushCommits{}
  246. }
  247. repoLink := fmt.Sprintf("%s%s/%s", setting.AppUrl, repoUserName, repoName)
  248. // if not the first commit, set the compareUrl
  249. if !strings.HasPrefix(oldCommitID, "0000000") {
  250. commit.CompareUrl = fmt.Sprintf("%s/%s/compare/%s...%s", repoUserName, repoName, oldCommitID, newCommitID)
  251. }
  252. bs, err := json.Marshal(commit)
  253. if err != nil {
  254. return fmt.Errorf("Marshal: %v", err)
  255. }
  256. refName := git.RefEndName(refFullName)
  257. // Change repository bare status and update last updated time.
  258. repo, err := GetRepositoryByName(repoUserID, repoName)
  259. if err != nil {
  260. return fmt.Errorf("GetRepositoryByName: %v", err)
  261. }
  262. repo.IsBare = false
  263. if err = UpdateRepository(repo, false); err != nil {
  264. return fmt.Errorf("UpdateRepository: %v", err)
  265. }
  266. u, err := GetUserByID(userID)
  267. if err != nil {
  268. return fmt.Errorf("GetUserByID: %v", err)
  269. }
  270. err = updateIssuesCommit(u, repo, repoUserName, repoName, commit.Commits)
  271. if err != nil {
  272. log.Debug("updateIssuesCommit: ", err)
  273. }
  274. if err = NotifyWatchers(&Action{
  275. ActUserID: u.Id,
  276. ActUserName: userName,
  277. ActEmail: actEmail,
  278. OpType: opType,
  279. Content: string(bs),
  280. RepoID: repo.ID,
  281. RepoUserName: repoUserName,
  282. RepoName: repoName,
  283. RefName: refName,
  284. IsPrivate: repo.IsPrivate,
  285. }); err != nil {
  286. return errors.New("NotifyWatchers: " + err.Error())
  287. }
  288. // New push event hook.
  289. if err := repo.GetOwner(); err != nil {
  290. return errors.New("GetOwner: " + err.Error())
  291. }
  292. ws, err := GetActiveWebhooksByRepoId(repo.ID)
  293. if err != nil {
  294. return errors.New("GetActiveWebhooksByRepoId: " + err.Error())
  295. }
  296. // check if repo belongs to org and append additional webhooks
  297. if repo.Owner.IsOrganization() {
  298. // get hooks for org
  299. orgws, err := GetActiveWebhooksByOrgId(repo.OwnerID)
  300. if err != nil {
  301. return errors.New("GetActiveWebhooksByOrgId: " + err.Error())
  302. }
  303. ws = append(ws, orgws...)
  304. }
  305. if len(ws) == 0 {
  306. return nil
  307. }
  308. pusher_email, pusher_name := "", ""
  309. pusher, err := GetUserByName(userName)
  310. if err == nil {
  311. pusher_email = pusher.Email
  312. pusher_name = pusher.GetFullNameFallback()
  313. }
  314. commits := make([]*PayloadCommit, len(commit.Commits))
  315. for i, cmt := range commit.Commits {
  316. author_username := ""
  317. author, err := GetUserByEmail(cmt.AuthorEmail)
  318. if err == nil {
  319. author_username = author.Name
  320. }
  321. commits[i] = &PayloadCommit{
  322. Id: cmt.Sha1,
  323. Message: cmt.Message,
  324. Url: fmt.Sprintf("%s/commit/%s", repoLink, cmt.Sha1),
  325. Author: &PayloadAuthor{
  326. Name: cmt.AuthorName,
  327. Email: cmt.AuthorEmail,
  328. UserName: author_username,
  329. },
  330. }
  331. }
  332. p := &Payload{
  333. Ref: refFullName,
  334. Commits: commits,
  335. Repo: &PayloadRepo{
  336. Id: repo.ID,
  337. Name: repo.LowerName,
  338. Url: repoLink,
  339. Description: repo.Description,
  340. Website: repo.Website,
  341. Watchers: repo.NumWatches,
  342. Owner: &PayloadAuthor{
  343. Name: repo.Owner.GetFullNameFallback(),
  344. Email: repo.Owner.Email,
  345. UserName: repo.Owner.Name,
  346. },
  347. Private: repo.IsPrivate,
  348. },
  349. Pusher: &PayloadAuthor{
  350. Name: pusher_name,
  351. Email: pusher_email,
  352. UserName: userName,
  353. },
  354. Before: oldCommitID,
  355. After: newCommitID,
  356. CompareUrl: setting.AppUrl + commit.CompareUrl,
  357. }
  358. for _, w := range ws {
  359. w.GetEvent()
  360. if !w.HasPushEvent() {
  361. continue
  362. }
  363. var payload BasePayload
  364. switch w.HookTaskType {
  365. case SLACK:
  366. s, err := GetSlackPayload(p, w.Meta)
  367. if err != nil {
  368. return errors.New("action.GetSlackPayload: " + err.Error())
  369. }
  370. payload = s
  371. default:
  372. payload = p
  373. p.Secret = w.Secret
  374. }
  375. if err = CreateHookTask(&HookTask{
  376. RepoID: repo.ID,
  377. HookID: w.ID,
  378. Type: w.HookTaskType,
  379. Url: w.URL,
  380. BasePayload: payload,
  381. ContentType: w.ContentType,
  382. EventType: HOOK_EVENT_PUSH,
  383. IsSsl: w.IsSSL,
  384. }); err != nil {
  385. return fmt.Errorf("CreateHookTask: %v", err)
  386. }
  387. }
  388. return nil
  389. }
  390. func newRepoAction(e Engine, u *User, repo *Repository) (err error) {
  391. if err = notifyWatchers(e, &Action{
  392. ActUserID: u.Id,
  393. ActUserName: u.Name,
  394. ActEmail: u.Email,
  395. OpType: CREATE_REPO,
  396. RepoID: repo.ID,
  397. RepoUserName: repo.Owner.Name,
  398. RepoName: repo.Name,
  399. IsPrivate: repo.IsPrivate,
  400. }); err != nil {
  401. return fmt.Errorf("notify watchers '%d/%s'", u.Id, repo.ID)
  402. }
  403. log.Trace("action.NewRepoAction: %s/%s", u.Name, repo.Name)
  404. return err
  405. }
  406. // NewRepoAction adds new action for creating repository.
  407. func NewRepoAction(u *User, repo *Repository) (err error) {
  408. return newRepoAction(x, u, repo)
  409. }
  410. func transferRepoAction(e Engine, actUser, oldOwner, newOwner *User, repo *Repository) (err error) {
  411. action := &Action{
  412. ActUserID: actUser.Id,
  413. ActUserName: actUser.Name,
  414. ActEmail: actUser.Email,
  415. OpType: TRANSFER_REPO,
  416. RepoID: repo.ID,
  417. RepoUserName: newOwner.Name,
  418. RepoName: repo.Name,
  419. IsPrivate: repo.IsPrivate,
  420. Content: path.Join(oldOwner.LowerName, repo.LowerName),
  421. }
  422. if err = notifyWatchers(e, action); err != nil {
  423. return fmt.Errorf("notify watchers '%d/%s'", actUser.Id, repo.ID)
  424. }
  425. // Remove watch for organization.
  426. if repo.Owner.IsOrganization() {
  427. if err = watchRepo(e, repo.Owner.Id, repo.ID, false); err != nil {
  428. return fmt.Errorf("watch repository: %v", err)
  429. }
  430. }
  431. log.Trace("action.TransferRepoAction: %s/%s", actUser.Name, repo.Name)
  432. return nil
  433. }
  434. // TransferRepoAction adds new action for transferring repository.
  435. func TransferRepoAction(actUser, oldOwner, newOwner *User, repo *Repository) (err error) {
  436. return transferRepoAction(x, actUser, oldOwner, newOwner, repo)
  437. }
  438. // GetFeeds returns action list of given user in given context.
  439. func GetFeeds(uid, offset int64, isProfile bool) ([]*Action, error) {
  440. actions := make([]*Action, 0, 20)
  441. sess := x.Limit(20, int(offset)).Desc("id").Where("user_id=?", uid)
  442. if isProfile {
  443. sess.And("is_private=?", false).And("act_user_id=?", uid)
  444. }
  445. err := sess.Find(&actions)
  446. return actions, err
  447. }