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

action.go 27KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package models
  6. import (
  7. "encoding/json"
  8. "fmt"
  9. "html"
  10. "path"
  11. "regexp"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "unicode"
  16. "code.gitea.io/gitea/modules/base"
  17. "code.gitea.io/gitea/modules/git"
  18. "code.gitea.io/gitea/modules/log"
  19. "code.gitea.io/gitea/modules/setting"
  20. api "code.gitea.io/gitea/modules/structs"
  21. "code.gitea.io/gitea/modules/util"
  22. "github.com/Unknwon/com"
  23. "xorm.io/builder"
  24. )
  25. // ActionType represents the type of an action.
  26. type ActionType int
  27. // Possible action types.
  28. const (
  29. ActionCreateRepo ActionType = iota + 1 // 1
  30. ActionRenameRepo // 2
  31. ActionStarRepo // 3
  32. ActionWatchRepo // 4
  33. ActionCommitRepo // 5
  34. ActionCreateIssue // 6
  35. ActionCreatePullRequest // 7
  36. ActionTransferRepo // 8
  37. ActionPushTag // 9
  38. ActionCommentIssue // 10
  39. ActionMergePullRequest // 11
  40. ActionCloseIssue // 12
  41. ActionReopenIssue // 13
  42. ActionClosePullRequest // 14
  43. ActionReopenPullRequest // 15
  44. ActionDeleteTag // 16
  45. ActionDeleteBranch // 17
  46. ActionMirrorSyncPush // 18
  47. ActionMirrorSyncCreate // 19
  48. ActionMirrorSyncDelete // 20
  49. )
  50. var (
  51. // Same as GitHub. See
  52. // https://help.github.com/articles/closing-issues-via-commit-messages
  53. issueCloseKeywords = []string{"close", "closes", "closed", "fix", "fixes", "fixed", "resolve", "resolves", "resolved"}
  54. issueReopenKeywords = []string{"reopen", "reopens", "reopened"}
  55. issueCloseKeywordsPat, issueReopenKeywordsPat *regexp.Regexp
  56. issueReferenceKeywordsPat *regexp.Regexp
  57. )
  58. const issueRefRegexpStr = `(?:([0-9a-zA-Z-_\.]+)/([0-9a-zA-Z-_\.]+))?(#[0-9]+)+`
  59. func assembleKeywordsPattern(words []string) string {
  60. return fmt.Sprintf(`(?i)(?:%s)(?::?) %s`, strings.Join(words, "|"), issueRefRegexpStr)
  61. }
  62. func init() {
  63. issueCloseKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueCloseKeywords))
  64. issueReopenKeywordsPat = regexp.MustCompile(assembleKeywordsPattern(issueReopenKeywords))
  65. issueReferenceKeywordsPat = regexp.MustCompile(issueRefRegexpStr)
  66. }
  67. // Action represents user operation type and other information to
  68. // repository. It implemented interface base.Actioner so that can be
  69. // used in template render.
  70. type Action struct {
  71. ID int64 `xorm:"pk autoincr"`
  72. UserID int64 `xorm:"INDEX"` // Receiver user id.
  73. OpType ActionType
  74. ActUserID int64 `xorm:"INDEX"` // Action user id.
  75. ActUser *User `xorm:"-"`
  76. RepoID int64 `xorm:"INDEX"`
  77. Repo *Repository `xorm:"-"`
  78. CommentID int64 `xorm:"INDEX"`
  79. Comment *Comment `xorm:"-"`
  80. IsDeleted bool `xorm:"INDEX NOT NULL DEFAULT false"`
  81. RefName string
  82. IsPrivate bool `xorm:"INDEX NOT NULL DEFAULT false"`
  83. Content string `xorm:"TEXT"`
  84. CreatedUnix util.TimeStamp `xorm:"INDEX created"`
  85. }
  86. // GetOpType gets the ActionType of this action.
  87. func (a *Action) GetOpType() ActionType {
  88. return a.OpType
  89. }
  90. func (a *Action) loadActUser() {
  91. if a.ActUser != nil {
  92. return
  93. }
  94. var err error
  95. a.ActUser, err = GetUserByID(a.ActUserID)
  96. if err == nil {
  97. return
  98. } else if IsErrUserNotExist(err) {
  99. a.ActUser = NewGhostUser()
  100. } else {
  101. log.Error("GetUserByID(%d): %v", a.ActUserID, err)
  102. }
  103. }
  104. func (a *Action) loadRepo() {
  105. if a.Repo != nil {
  106. return
  107. }
  108. var err error
  109. a.Repo, err = GetRepositoryByID(a.RepoID)
  110. if err != nil {
  111. log.Error("GetRepositoryByID(%d): %v", a.RepoID, err)
  112. }
  113. }
  114. // GetActFullName gets the action's user full name.
  115. func (a *Action) GetActFullName() string {
  116. a.loadActUser()
  117. return a.ActUser.FullName
  118. }
  119. // GetActUserName gets the action's user name.
  120. func (a *Action) GetActUserName() string {
  121. a.loadActUser()
  122. return a.ActUser.Name
  123. }
  124. // ShortActUserName gets the action's user name trimmed to max 20
  125. // chars.
  126. func (a *Action) ShortActUserName() string {
  127. return base.EllipsisString(a.GetActUserName(), 20)
  128. }
  129. // GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME
  130. func (a *Action) GetDisplayName() string {
  131. if setting.UI.DefaultShowFullName {
  132. return a.GetActFullName()
  133. }
  134. return a.ShortActUserName()
  135. }
  136. // GetDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
  137. func (a *Action) GetDisplayNameTitle() string {
  138. if setting.UI.DefaultShowFullName {
  139. return a.ShortActUserName()
  140. }
  141. return a.GetActFullName()
  142. }
  143. // GetActAvatar the action's user's avatar link
  144. func (a *Action) GetActAvatar() string {
  145. a.loadActUser()
  146. return a.ActUser.RelAvatarLink()
  147. }
  148. // GetRepoUserName returns the name of the action repository owner.
  149. func (a *Action) GetRepoUserName() string {
  150. a.loadRepo()
  151. return a.Repo.MustOwner().Name
  152. }
  153. // ShortRepoUserName returns the name of the action repository owner
  154. // trimmed to max 20 chars.
  155. func (a *Action) ShortRepoUserName() string {
  156. return base.EllipsisString(a.GetRepoUserName(), 20)
  157. }
  158. // GetRepoName returns the name of the action repository.
  159. func (a *Action) GetRepoName() string {
  160. a.loadRepo()
  161. return a.Repo.Name
  162. }
  163. // ShortRepoName returns the name of the action repository
  164. // trimmed to max 33 chars.
  165. func (a *Action) ShortRepoName() string {
  166. return base.EllipsisString(a.GetRepoName(), 33)
  167. }
  168. // GetRepoPath returns the virtual path to the action repository.
  169. func (a *Action) GetRepoPath() string {
  170. return path.Join(a.GetRepoUserName(), a.GetRepoName())
  171. }
  172. // ShortRepoPath returns the virtual path to the action repository
  173. // trimmed to max 20 + 1 + 33 chars.
  174. func (a *Action) ShortRepoPath() string {
  175. return path.Join(a.ShortRepoUserName(), a.ShortRepoName())
  176. }
  177. // GetRepoLink returns relative link to action repository.
  178. func (a *Action) GetRepoLink() string {
  179. if len(setting.AppSubURL) > 0 {
  180. return path.Join(setting.AppSubURL, a.GetRepoPath())
  181. }
  182. return "/" + a.GetRepoPath()
  183. }
  184. // GetRepositoryFromMatch returns a *Repository from a username and repo strings
  185. func GetRepositoryFromMatch(ownerName string, repoName string) (*Repository, error) {
  186. var err error
  187. refRepo, err := GetRepositoryByOwnerAndName(ownerName, repoName)
  188. if err != nil {
  189. if IsErrRepoNotExist(err) {
  190. log.Warn("Repository referenced in commit but does not exist: %v", err)
  191. return nil, err
  192. }
  193. log.Error("GetRepositoryByOwnerAndName: %v", err)
  194. return nil, err
  195. }
  196. return refRepo, nil
  197. }
  198. // GetCommentLink returns link to action comment.
  199. func (a *Action) GetCommentLink() string {
  200. return a.getCommentLink(x)
  201. }
  202. func (a *Action) getCommentLink(e Engine) string {
  203. if a == nil {
  204. return "#"
  205. }
  206. if a.Comment == nil && a.CommentID != 0 {
  207. a.Comment, _ = GetCommentByID(a.CommentID)
  208. }
  209. if a.Comment != nil {
  210. return a.Comment.HTMLURL()
  211. }
  212. if len(a.GetIssueInfos()) == 0 {
  213. return "#"
  214. }
  215. //Return link to issue
  216. issueIDString := a.GetIssueInfos()[0]
  217. issueID, err := strconv.ParseInt(issueIDString, 10, 64)
  218. if err != nil {
  219. return "#"
  220. }
  221. issue, err := getIssueByID(e, issueID)
  222. if err != nil {
  223. return "#"
  224. }
  225. if err = issue.loadRepo(e); err != nil {
  226. return "#"
  227. }
  228. return issue.HTMLURL()
  229. }
  230. // GetBranch returns the action's repository branch.
  231. func (a *Action) GetBranch() string {
  232. return a.RefName
  233. }
  234. // GetContent returns the action's content.
  235. func (a *Action) GetContent() string {
  236. return a.Content
  237. }
  238. // GetCreate returns the action creation time.
  239. func (a *Action) GetCreate() time.Time {
  240. return a.CreatedUnix.AsTime()
  241. }
  242. // GetIssueInfos returns a list of issues associated with
  243. // the action.
  244. func (a *Action) GetIssueInfos() []string {
  245. return strings.SplitN(a.Content, "|", 2)
  246. }
  247. // GetIssueTitle returns the title of first issue associated
  248. // with the action.
  249. func (a *Action) GetIssueTitle() string {
  250. index := com.StrTo(a.GetIssueInfos()[0]).MustInt64()
  251. issue, err := GetIssueByIndex(a.RepoID, index)
  252. if err != nil {
  253. log.Error("GetIssueByIndex: %v", err)
  254. return "500 when get issue"
  255. }
  256. return issue.Title
  257. }
  258. // GetIssueContent returns the content of first issue associated with
  259. // this action.
  260. func (a *Action) GetIssueContent() string {
  261. index := com.StrTo(a.GetIssueInfos()[0]).MustInt64()
  262. issue, err := GetIssueByIndex(a.RepoID, index)
  263. if err != nil {
  264. log.Error("GetIssueByIndex: %v", err)
  265. return "500 when get issue"
  266. }
  267. return issue.Content
  268. }
  269. func newRepoAction(e Engine, u *User, repo *Repository) (err error) {
  270. if err = notifyWatchers(e, &Action{
  271. ActUserID: u.ID,
  272. ActUser: u,
  273. OpType: ActionCreateRepo,
  274. RepoID: repo.ID,
  275. Repo: repo,
  276. IsPrivate: repo.IsPrivate,
  277. }); err != nil {
  278. return fmt.Errorf("notify watchers '%d/%d': %v", u.ID, repo.ID, err)
  279. }
  280. log.Trace("action.newRepoAction: %s/%s", u.Name, repo.Name)
  281. return err
  282. }
  283. // NewRepoAction adds new action for creating repository.
  284. func NewRepoAction(u *User, repo *Repository) (err error) {
  285. return newRepoAction(x, u, repo)
  286. }
  287. func renameRepoAction(e Engine, actUser *User, oldRepoName string, repo *Repository) (err error) {
  288. if err = notifyWatchers(e, &Action{
  289. ActUserID: actUser.ID,
  290. ActUser: actUser,
  291. OpType: ActionRenameRepo,
  292. RepoID: repo.ID,
  293. Repo: repo,
  294. IsPrivate: repo.IsPrivate,
  295. Content: oldRepoName,
  296. }); err != nil {
  297. return fmt.Errorf("notify watchers: %v", err)
  298. }
  299. log.Trace("action.renameRepoAction: %s/%s", actUser.Name, repo.Name)
  300. return nil
  301. }
  302. // RenameRepoAction adds new action for renaming a repository.
  303. func RenameRepoAction(actUser *User, oldRepoName string, repo *Repository) error {
  304. return renameRepoAction(x, actUser, oldRepoName, repo)
  305. }
  306. func issueIndexTrimRight(c rune) bool {
  307. return !unicode.IsDigit(c)
  308. }
  309. // PushCommit represents a commit in a push operation.
  310. type PushCommit struct {
  311. Sha1 string
  312. Message string
  313. AuthorEmail string
  314. AuthorName string
  315. CommitterEmail string
  316. CommitterName string
  317. Timestamp time.Time
  318. }
  319. // PushCommits represents list of commits in a push operation.
  320. type PushCommits struct {
  321. Len int
  322. Commits []*PushCommit
  323. CompareURL string
  324. avatars map[string]string
  325. emailUsers map[string]*User
  326. }
  327. // NewPushCommits creates a new PushCommits object.
  328. func NewPushCommits() *PushCommits {
  329. return &PushCommits{
  330. avatars: make(map[string]string),
  331. emailUsers: make(map[string]*User),
  332. }
  333. }
  334. // ToAPIPayloadCommits converts a PushCommits object to
  335. // api.PayloadCommit format.
  336. func (pc *PushCommits) ToAPIPayloadCommits(repoLink string) []*api.PayloadCommit {
  337. commits := make([]*api.PayloadCommit, len(pc.Commits))
  338. if pc.emailUsers == nil {
  339. pc.emailUsers = make(map[string]*User)
  340. }
  341. var err error
  342. for i, commit := range pc.Commits {
  343. authorUsername := ""
  344. author, ok := pc.emailUsers[commit.AuthorEmail]
  345. if !ok {
  346. author, err = GetUserByEmail(commit.AuthorEmail)
  347. if err == nil {
  348. authorUsername = author.Name
  349. pc.emailUsers[commit.AuthorEmail] = author
  350. }
  351. } else {
  352. authorUsername = author.Name
  353. }
  354. committerUsername := ""
  355. committer, ok := pc.emailUsers[commit.CommitterEmail]
  356. if !ok {
  357. committer, err = GetUserByEmail(commit.CommitterEmail)
  358. if err == nil {
  359. // TODO: check errors other than email not found.
  360. committerUsername = committer.Name
  361. pc.emailUsers[commit.CommitterEmail] = committer
  362. }
  363. } else {
  364. committerUsername = committer.Name
  365. }
  366. commits[i] = &api.PayloadCommit{
  367. ID: commit.Sha1,
  368. Message: commit.Message,
  369. URL: fmt.Sprintf("%s/commit/%s", repoLink, commit.Sha1),
  370. Author: &api.PayloadUser{
  371. Name: commit.AuthorName,
  372. Email: commit.AuthorEmail,
  373. UserName: authorUsername,
  374. },
  375. Committer: &api.PayloadUser{
  376. Name: commit.CommitterName,
  377. Email: commit.CommitterEmail,
  378. UserName: committerUsername,
  379. },
  380. Timestamp: commit.Timestamp,
  381. }
  382. }
  383. return commits
  384. }
  385. // AvatarLink tries to match user in database with e-mail
  386. // in order to show custom avatar, and falls back to general avatar link.
  387. func (pc *PushCommits) AvatarLink(email string) string {
  388. avatar, ok := pc.avatars[email]
  389. if ok {
  390. return avatar
  391. }
  392. u, ok := pc.emailUsers[email]
  393. if !ok {
  394. var err error
  395. u, err = GetUserByEmail(email)
  396. if err != nil {
  397. pc.avatars[email] = base.AvatarLink(email)
  398. if !IsErrUserNotExist(err) {
  399. log.Error("GetUserByEmail: %v", err)
  400. return ""
  401. }
  402. } else {
  403. pc.emailUsers[email] = u
  404. }
  405. }
  406. if u != nil {
  407. pc.avatars[email] = u.RelAvatarLink()
  408. }
  409. return pc.avatars[email]
  410. }
  411. // getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue
  412. // if the provided ref is misformatted or references a non-existent issue.
  413. func getIssueFromRef(repo *Repository, ref string) (*Issue, error) {
  414. ref = ref[strings.IndexByte(ref, ' ')+1:]
  415. ref = strings.TrimRightFunc(ref, issueIndexTrimRight)
  416. var refRepo *Repository
  417. poundIndex := strings.IndexByte(ref, '#')
  418. if poundIndex < 0 {
  419. return nil, nil
  420. } else if poundIndex == 0 {
  421. refRepo = repo
  422. } else {
  423. slashIndex := strings.IndexByte(ref, '/')
  424. if slashIndex < 0 || slashIndex >= poundIndex {
  425. return nil, nil
  426. }
  427. ownerName := ref[:slashIndex]
  428. repoName := ref[slashIndex+1 : poundIndex]
  429. var err error
  430. refRepo, err = GetRepositoryByOwnerAndName(ownerName, repoName)
  431. if err != nil {
  432. if IsErrRepoNotExist(err) {
  433. return nil, nil
  434. }
  435. return nil, err
  436. }
  437. }
  438. issueIndex, err := strconv.ParseInt(ref[poundIndex+1:], 10, 64)
  439. if err != nil {
  440. return nil, nil
  441. }
  442. issue, err := GetIssueByIndex(refRepo.ID, int64(issueIndex))
  443. if err != nil {
  444. if IsErrIssueNotExist(err) {
  445. return nil, nil
  446. }
  447. return nil, err
  448. }
  449. return issue, nil
  450. }
  451. func changeIssueStatus(repo *Repository, doer *User, ref string, refMarked map[int64]bool, status bool) error {
  452. issue, err := getIssueFromRef(repo, ref)
  453. if err != nil {
  454. return err
  455. }
  456. if issue == nil || refMarked[issue.ID] {
  457. return nil
  458. }
  459. refMarked[issue.ID] = true
  460. if issue.RepoID != repo.ID || issue.IsClosed == status {
  461. return nil
  462. }
  463. stopTimerIfAvailable := func(doer *User, issue *Issue) error {
  464. if StopwatchExists(doer.ID, issue.ID) {
  465. if err := CreateOrStopIssueStopwatch(doer, issue); err != nil {
  466. return err
  467. }
  468. }
  469. return nil
  470. }
  471. issue.Repo = repo
  472. if err = issue.ChangeStatus(doer, status); err != nil {
  473. // Don't return an error when dependencies are open as this would let the push fail
  474. if IsErrDependenciesLeft(err) {
  475. return stopTimerIfAvailable(doer, issue)
  476. }
  477. return err
  478. }
  479. return stopTimerIfAvailable(doer, issue)
  480. }
  481. // UpdateIssuesCommit checks if issues are manipulated by commit message.
  482. func UpdateIssuesCommit(doer *User, repo *Repository, commits []*PushCommit, branchName string) error {
  483. // Commits are appended in the reverse order.
  484. for i := len(commits) - 1; i >= 0; i-- {
  485. c := commits[i]
  486. refMarked := make(map[int64]bool)
  487. var refRepo *Repository
  488. var err error
  489. for _, m := range issueReferenceKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
  490. if len(m[3]) == 0 {
  491. continue
  492. }
  493. ref := m[3]
  494. // issue is from another repo
  495. if len(m[1]) > 0 && len(m[2]) > 0 {
  496. refRepo, err = GetRepositoryFromMatch(string(m[1]), string(m[2]))
  497. if err != nil {
  498. continue
  499. }
  500. } else {
  501. refRepo = repo
  502. }
  503. issue, err := getIssueFromRef(refRepo, ref)
  504. if err != nil {
  505. return err
  506. }
  507. if issue == nil || refMarked[issue.ID] {
  508. continue
  509. }
  510. refMarked[issue.ID] = true
  511. message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, html.EscapeString(c.Message))
  512. if err = CreateRefComment(doer, refRepo, issue, message, c.Sha1); err != nil {
  513. return err
  514. }
  515. }
  516. // Change issue status only if the commit has been pushed to the default branch.
  517. // and if the repo is configured to allow only that
  518. if repo.DefaultBranch != branchName && !repo.CloseIssuesViaCommitInAnyBranch {
  519. continue
  520. }
  521. refMarked = make(map[int64]bool)
  522. for _, m := range issueCloseKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
  523. if len(m[3]) == 0 {
  524. continue
  525. }
  526. ref := m[3]
  527. // issue is from another repo
  528. if len(m[1]) > 0 && len(m[2]) > 0 {
  529. refRepo, err = GetRepositoryFromMatch(string(m[1]), string(m[2]))
  530. if err != nil {
  531. continue
  532. }
  533. } else {
  534. refRepo = repo
  535. }
  536. perm, err := GetUserRepoPermission(refRepo, doer)
  537. if err != nil {
  538. return err
  539. }
  540. // only close issues in another repo if user has push access
  541. if perm.CanWrite(UnitTypeCode) {
  542. if err := changeIssueStatus(refRepo, doer, ref, refMarked, true); err != nil {
  543. return err
  544. }
  545. }
  546. }
  547. // It is conflict to have close and reopen at same time, so refsMarked doesn't need to reinit here.
  548. for _, m := range issueReopenKeywordsPat.FindAllStringSubmatch(c.Message, -1) {
  549. if len(m[3]) == 0 {
  550. continue
  551. }
  552. ref := m[3]
  553. // issue is from another repo
  554. if len(m[1]) > 0 && len(m[2]) > 0 {
  555. refRepo, err = GetRepositoryFromMatch(string(m[1]), string(m[2]))
  556. if err != nil {
  557. continue
  558. }
  559. } else {
  560. refRepo = repo
  561. }
  562. perm, err := GetUserRepoPermission(refRepo, doer)
  563. if err != nil {
  564. return err
  565. }
  566. // only reopen issues in another repo if user has push access
  567. if perm.CanWrite(UnitTypeCode) {
  568. if err := changeIssueStatus(refRepo, doer, ref, refMarked, false); err != nil {
  569. return err
  570. }
  571. }
  572. }
  573. }
  574. return nil
  575. }
  576. // CommitRepoActionOptions represent options of a new commit action.
  577. type CommitRepoActionOptions struct {
  578. PusherName string
  579. RepoOwnerID int64
  580. RepoName string
  581. RefFullName string
  582. OldCommitID string
  583. NewCommitID string
  584. Commits *PushCommits
  585. }
  586. // CommitRepoAction adds new commit action to the repository, and prepare
  587. // corresponding webhooks.
  588. func CommitRepoAction(opts CommitRepoActionOptions) error {
  589. pusher, err := GetUserByName(opts.PusherName)
  590. if err != nil {
  591. return fmt.Errorf("GetUserByName [%s]: %v", opts.PusherName, err)
  592. }
  593. repo, err := GetRepositoryByName(opts.RepoOwnerID, opts.RepoName)
  594. if err != nil {
  595. return fmt.Errorf("GetRepositoryByName [owner_id: %d, name: %s]: %v", opts.RepoOwnerID, opts.RepoName, err)
  596. }
  597. refName := git.RefEndName(opts.RefFullName)
  598. // Change default branch and empty status only if pushed ref is non-empty branch.
  599. if repo.IsEmpty && opts.NewCommitID != git.EmptySHA && strings.HasPrefix(opts.RefFullName, git.BranchPrefix) {
  600. repo.DefaultBranch = refName
  601. repo.IsEmpty = false
  602. }
  603. // Change repository empty status and update last updated time.
  604. if err = UpdateRepository(repo, false); err != nil {
  605. return fmt.Errorf("UpdateRepository: %v", err)
  606. }
  607. isNewBranch := false
  608. opType := ActionCommitRepo
  609. // Check it's tag push or branch.
  610. if strings.HasPrefix(opts.RefFullName, git.TagPrefix) {
  611. opType = ActionPushTag
  612. if opts.NewCommitID == git.EmptySHA {
  613. opType = ActionDeleteTag
  614. }
  615. opts.Commits = &PushCommits{}
  616. } else if opts.NewCommitID == git.EmptySHA {
  617. opType = ActionDeleteBranch
  618. opts.Commits = &PushCommits{}
  619. } else {
  620. // if not the first commit, set the compare URL.
  621. if opts.OldCommitID == git.EmptySHA {
  622. isNewBranch = true
  623. } else {
  624. opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
  625. }
  626. if err = UpdateIssuesCommit(pusher, repo, opts.Commits.Commits, refName); err != nil {
  627. log.Error("updateIssuesCommit: %v", err)
  628. }
  629. }
  630. if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum {
  631. opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum]
  632. }
  633. data, err := json.Marshal(opts.Commits)
  634. if err != nil {
  635. return fmt.Errorf("Marshal: %v", err)
  636. }
  637. if err = NotifyWatchers(&Action{
  638. ActUserID: pusher.ID,
  639. ActUser: pusher,
  640. OpType: opType,
  641. Content: string(data),
  642. RepoID: repo.ID,
  643. Repo: repo,
  644. RefName: refName,
  645. IsPrivate: repo.IsPrivate,
  646. }); err != nil {
  647. return fmt.Errorf("NotifyWatchers: %v", err)
  648. }
  649. defer func() {
  650. go HookQueue.Add(repo.ID)
  651. }()
  652. apiPusher := pusher.APIFormat()
  653. apiRepo := repo.APIFormat(AccessModeNone)
  654. var shaSum string
  655. var isHookEventPush = false
  656. switch opType {
  657. case ActionCommitRepo: // Push
  658. isHookEventPush = true
  659. if isNewBranch {
  660. gitRepo, err := git.OpenRepository(repo.RepoPath())
  661. if err != nil {
  662. log.Error("OpenRepository[%s]: %v", repo.RepoPath(), err)
  663. }
  664. shaSum, err = gitRepo.GetBranchCommitID(refName)
  665. if err != nil {
  666. log.Error("GetBranchCommitID[%s]: %v", opts.RefFullName, err)
  667. }
  668. if err = PrepareWebhooks(repo, HookEventCreate, &api.CreatePayload{
  669. Ref: refName,
  670. Sha: shaSum,
  671. RefType: "branch",
  672. Repo: apiRepo,
  673. Sender: apiPusher,
  674. }); err != nil {
  675. return fmt.Errorf("PrepareWebhooks: %v", err)
  676. }
  677. }
  678. case ActionDeleteBranch: // Delete Branch
  679. isHookEventPush = true
  680. if err = PrepareWebhooks(repo, HookEventDelete, &api.DeletePayload{
  681. Ref: refName,
  682. RefType: "branch",
  683. PusherType: api.PusherTypeUser,
  684. Repo: apiRepo,
  685. Sender: apiPusher,
  686. }); err != nil {
  687. return fmt.Errorf("PrepareWebhooks.(delete branch): %v", err)
  688. }
  689. case ActionPushTag: // Create
  690. isHookEventPush = true
  691. gitRepo, err := git.OpenRepository(repo.RepoPath())
  692. if err != nil {
  693. log.Error("OpenRepository[%s]: %v", repo.RepoPath(), err)
  694. }
  695. shaSum, err = gitRepo.GetTagCommitID(refName)
  696. if err != nil {
  697. log.Error("GetTagCommitID[%s]: %v", opts.RefFullName, err)
  698. }
  699. if err = PrepareWebhooks(repo, HookEventCreate, &api.CreatePayload{
  700. Ref: refName,
  701. Sha: shaSum,
  702. RefType: "tag",
  703. Repo: apiRepo,
  704. Sender: apiPusher,
  705. }); err != nil {
  706. return fmt.Errorf("PrepareWebhooks: %v", err)
  707. }
  708. case ActionDeleteTag: // Delete Tag
  709. isHookEventPush = true
  710. if err = PrepareWebhooks(repo, HookEventDelete, &api.DeletePayload{
  711. Ref: refName,
  712. RefType: "tag",
  713. PusherType: api.PusherTypeUser,
  714. Repo: apiRepo,
  715. Sender: apiPusher,
  716. }); err != nil {
  717. return fmt.Errorf("PrepareWebhooks.(delete tag): %v", err)
  718. }
  719. }
  720. if isHookEventPush {
  721. if err = PrepareWebhooks(repo, HookEventPush, &api.PushPayload{
  722. Ref: opts.RefFullName,
  723. Before: opts.OldCommitID,
  724. After: opts.NewCommitID,
  725. CompareURL: setting.AppURL + opts.Commits.CompareURL,
  726. Commits: opts.Commits.ToAPIPayloadCommits(repo.HTMLURL()),
  727. Repo: apiRepo,
  728. Pusher: apiPusher,
  729. Sender: apiPusher,
  730. }); err != nil {
  731. return fmt.Errorf("PrepareWebhooks: %v", err)
  732. }
  733. }
  734. return nil
  735. }
  736. func transferRepoAction(e Engine, doer, oldOwner *User, repo *Repository) (err error) {
  737. if err = notifyWatchers(e, &Action{
  738. ActUserID: doer.ID,
  739. ActUser: doer,
  740. OpType: ActionTransferRepo,
  741. RepoID: repo.ID,
  742. Repo: repo,
  743. IsPrivate: repo.IsPrivate,
  744. Content: path.Join(oldOwner.Name, repo.Name),
  745. }); err != nil {
  746. return fmt.Errorf("notifyWatchers: %v", err)
  747. }
  748. // Remove watch for organization.
  749. if oldOwner.IsOrganization() {
  750. if err = watchRepo(e, oldOwner.ID, repo.ID, false); err != nil {
  751. return fmt.Errorf("watchRepo [false]: %v", err)
  752. }
  753. }
  754. return nil
  755. }
  756. // TransferRepoAction adds new action for transferring repository,
  757. // the Owner field of repository is assumed to be new owner.
  758. func TransferRepoAction(doer, oldOwner *User, repo *Repository) error {
  759. return transferRepoAction(x, doer, oldOwner, repo)
  760. }
  761. func mergePullRequestAction(e Engine, doer *User, repo *Repository, issue *Issue) error {
  762. return notifyWatchers(e, &Action{
  763. ActUserID: doer.ID,
  764. ActUser: doer,
  765. OpType: ActionMergePullRequest,
  766. Content: fmt.Sprintf("%d|%s", issue.Index, issue.Title),
  767. RepoID: repo.ID,
  768. Repo: repo,
  769. IsPrivate: repo.IsPrivate,
  770. })
  771. }
  772. // MergePullRequestAction adds new action for merging pull request.
  773. func MergePullRequestAction(actUser *User, repo *Repository, pull *Issue) error {
  774. return mergePullRequestAction(x, actUser, repo, pull)
  775. }
  776. func mirrorSyncAction(e Engine, opType ActionType, repo *Repository, refName string, data []byte) error {
  777. if err := notifyWatchers(e, &Action{
  778. ActUserID: repo.OwnerID,
  779. ActUser: repo.MustOwner(),
  780. OpType: opType,
  781. RepoID: repo.ID,
  782. Repo: repo,
  783. IsPrivate: repo.IsPrivate,
  784. RefName: refName,
  785. Content: string(data),
  786. }); err != nil {
  787. return fmt.Errorf("notifyWatchers: %v", err)
  788. }
  789. return nil
  790. }
  791. // MirrorSyncPushActionOptions mirror synchronization action options.
  792. type MirrorSyncPushActionOptions struct {
  793. RefName string
  794. OldCommitID string
  795. NewCommitID string
  796. Commits *PushCommits
  797. }
  798. // MirrorSyncPushAction adds new action for mirror synchronization of pushed commits.
  799. func MirrorSyncPushAction(repo *Repository, opts MirrorSyncPushActionOptions) error {
  800. if len(opts.Commits.Commits) > setting.UI.FeedMaxCommitNum {
  801. opts.Commits.Commits = opts.Commits.Commits[:setting.UI.FeedMaxCommitNum]
  802. }
  803. apiCommits := opts.Commits.ToAPIPayloadCommits(repo.HTMLURL())
  804. opts.Commits.CompareURL = repo.ComposeCompareURL(opts.OldCommitID, opts.NewCommitID)
  805. apiPusher := repo.MustOwner().APIFormat()
  806. if err := PrepareWebhooks(repo, HookEventPush, &api.PushPayload{
  807. Ref: opts.RefName,
  808. Before: opts.OldCommitID,
  809. After: opts.NewCommitID,
  810. CompareURL: setting.AppURL + opts.Commits.CompareURL,
  811. Commits: apiCommits,
  812. Repo: repo.APIFormat(AccessModeOwner),
  813. Pusher: apiPusher,
  814. Sender: apiPusher,
  815. }); err != nil {
  816. return fmt.Errorf("PrepareWebhooks: %v", err)
  817. }
  818. data, err := json.Marshal(opts.Commits)
  819. if err != nil {
  820. return err
  821. }
  822. return mirrorSyncAction(x, ActionMirrorSyncPush, repo, opts.RefName, data)
  823. }
  824. // MirrorSyncCreateAction adds new action for mirror synchronization of new reference.
  825. func MirrorSyncCreateAction(repo *Repository, refName string) error {
  826. return mirrorSyncAction(x, ActionMirrorSyncCreate, repo, refName, nil)
  827. }
  828. // MirrorSyncDeleteAction adds new action for mirror synchronization of delete reference.
  829. func MirrorSyncDeleteAction(repo *Repository, refName string) error {
  830. return mirrorSyncAction(x, ActionMirrorSyncDelete, repo, refName, nil)
  831. }
  832. // GetFeedsOptions options for retrieving feeds
  833. type GetFeedsOptions struct {
  834. RequestedUser *User
  835. RequestingUserID int64
  836. IncludePrivate bool // include private actions
  837. OnlyPerformedBy bool // only actions performed by requested user
  838. IncludeDeleted bool // include deleted actions
  839. }
  840. // GetFeeds returns actions according to the provided options
  841. func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
  842. cond := builder.NewCond()
  843. var repoIDs []int64
  844. if opts.RequestedUser.IsOrganization() {
  845. env, err := opts.RequestedUser.AccessibleReposEnv(opts.RequestingUserID)
  846. if err != nil {
  847. return nil, fmt.Errorf("AccessibleReposEnv: %v", err)
  848. }
  849. if repoIDs, err = env.RepoIDs(1, opts.RequestedUser.NumRepos); err != nil {
  850. return nil, fmt.Errorf("GetUserRepositories: %v", err)
  851. }
  852. cond = cond.And(builder.In("repo_id", repoIDs))
  853. }
  854. cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
  855. if opts.OnlyPerformedBy {
  856. cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID})
  857. }
  858. if !opts.IncludePrivate {
  859. cond = cond.And(builder.Eq{"is_private": false})
  860. }
  861. if !opts.IncludeDeleted {
  862. cond = cond.And(builder.Eq{"is_deleted": false})
  863. }
  864. actions := make([]*Action, 0, 20)
  865. if err := x.Limit(20).Desc("id").Where(cond).Find(&actions); err != nil {
  866. return nil, fmt.Errorf("Find: %v", err)
  867. }
  868. if err := ActionList(actions).LoadAttributes(); err != nil {
  869. return nil, fmt.Errorf("LoadAttributes: %v", err)
  870. }
  871. return actions, nil
  872. }