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 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  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. "context"
  8. "fmt"
  9. "net/url"
  10. "path"
  11. "strconv"
  12. "strings"
  13. "time"
  14. "code.gitea.io/gitea/models/db"
  15. "code.gitea.io/gitea/models/organization"
  16. access_model "code.gitea.io/gitea/models/perm/access"
  17. repo_model "code.gitea.io/gitea/models/repo"
  18. "code.gitea.io/gitea/models/unit"
  19. user_model "code.gitea.io/gitea/models/user"
  20. "code.gitea.io/gitea/modules/base"
  21. "code.gitea.io/gitea/modules/git"
  22. "code.gitea.io/gitea/modules/log"
  23. "code.gitea.io/gitea/modules/setting"
  24. "code.gitea.io/gitea/modules/structs"
  25. "code.gitea.io/gitea/modules/timeutil"
  26. "code.gitea.io/gitea/modules/util"
  27. "xorm.io/builder"
  28. )
  29. // ActionType represents the type of an action.
  30. type ActionType int
  31. // Possible action types.
  32. const (
  33. ActionCreateRepo ActionType = iota + 1 // 1
  34. ActionRenameRepo // 2
  35. ActionStarRepo // 3
  36. ActionWatchRepo // 4
  37. ActionCommitRepo // 5
  38. ActionCreateIssue // 6
  39. ActionCreatePullRequest // 7
  40. ActionTransferRepo // 8
  41. ActionPushTag // 9
  42. ActionCommentIssue // 10
  43. ActionMergePullRequest // 11
  44. ActionCloseIssue // 12
  45. ActionReopenIssue // 13
  46. ActionClosePullRequest // 14
  47. ActionReopenPullRequest // 15
  48. ActionDeleteTag // 16
  49. ActionDeleteBranch // 17
  50. ActionMirrorSyncPush // 18
  51. ActionMirrorSyncCreate // 19
  52. ActionMirrorSyncDelete // 20
  53. ActionApprovePullRequest // 21
  54. ActionRejectPullRequest // 22
  55. ActionCommentPull // 23
  56. ActionPublishRelease // 24
  57. ActionPullReviewDismissed // 25
  58. ActionPullRequestReadyForReview // 26
  59. )
  60. // Action represents user operation type and other information to
  61. // repository. It implemented interface base.Actioner so that can be
  62. // used in template render.
  63. type Action struct {
  64. ID int64 `xorm:"pk autoincr"`
  65. UserID int64 `xorm:"INDEX"` // Receiver user id.
  66. OpType ActionType
  67. ActUserID int64 `xorm:"INDEX"` // Action user id.
  68. ActUser *user_model.User `xorm:"-"`
  69. RepoID int64 `xorm:"INDEX"`
  70. Repo *repo_model.Repository `xorm:"-"`
  71. CommentID int64 `xorm:"INDEX"`
  72. Comment *Comment `xorm:"-"`
  73. IsDeleted bool `xorm:"INDEX NOT NULL DEFAULT false"`
  74. RefName string
  75. IsPrivate bool `xorm:"INDEX NOT NULL DEFAULT false"`
  76. Content string `xorm:"TEXT"`
  77. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  78. }
  79. func init() {
  80. db.RegisterModel(new(Action))
  81. }
  82. // GetOpType gets the ActionType of this action.
  83. func (a *Action) GetOpType() ActionType {
  84. return a.OpType
  85. }
  86. // LoadActUser loads a.ActUser
  87. func (a *Action) LoadActUser() {
  88. if a.ActUser != nil {
  89. return
  90. }
  91. var err error
  92. a.ActUser, err = user_model.GetUserByID(a.ActUserID)
  93. if err == nil {
  94. return
  95. } else if user_model.IsErrUserNotExist(err) {
  96. a.ActUser = user_model.NewGhostUser()
  97. } else {
  98. log.Error("GetUserByID(%d): %v", a.ActUserID, err)
  99. }
  100. }
  101. func (a *Action) loadRepo() {
  102. if a.Repo != nil {
  103. return
  104. }
  105. var err error
  106. a.Repo, err = repo_model.GetRepositoryByID(a.RepoID)
  107. if err != nil {
  108. log.Error("repo_model.GetRepositoryByID(%d): %v", a.RepoID, err)
  109. }
  110. }
  111. // GetActFullName gets the action's user full name.
  112. func (a *Action) GetActFullName() string {
  113. a.LoadActUser()
  114. return a.ActUser.FullName
  115. }
  116. // GetActUserName gets the action's user name.
  117. func (a *Action) GetActUserName() string {
  118. a.LoadActUser()
  119. return a.ActUser.Name
  120. }
  121. // ShortActUserName gets the action's user name trimmed to max 20
  122. // chars.
  123. func (a *Action) ShortActUserName() string {
  124. return base.EllipsisString(a.GetActUserName(), 20)
  125. }
  126. // GetDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
  127. func (a *Action) GetDisplayName() string {
  128. if setting.UI.DefaultShowFullName {
  129. trimmedFullName := strings.TrimSpace(a.GetActFullName())
  130. if len(trimmedFullName) > 0 {
  131. return trimmedFullName
  132. }
  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. // GetRepoUserName returns the name of the action repository owner.
  144. func (a *Action) GetRepoUserName() string {
  145. a.loadRepo()
  146. return a.Repo.OwnerName
  147. }
  148. // ShortRepoUserName returns the name of the action repository owner
  149. // trimmed to max 20 chars.
  150. func (a *Action) ShortRepoUserName() string {
  151. return base.EllipsisString(a.GetRepoUserName(), 20)
  152. }
  153. // GetRepoName returns the name of the action repository.
  154. func (a *Action) GetRepoName() string {
  155. a.loadRepo()
  156. return a.Repo.Name
  157. }
  158. // ShortRepoName returns the name of the action repository
  159. // trimmed to max 33 chars.
  160. func (a *Action) ShortRepoName() string {
  161. return base.EllipsisString(a.GetRepoName(), 33)
  162. }
  163. // GetRepoPath returns the virtual path to the action repository.
  164. func (a *Action) GetRepoPath() string {
  165. return path.Join(a.GetRepoUserName(), a.GetRepoName())
  166. }
  167. // ShortRepoPath returns the virtual path to the action repository
  168. // trimmed to max 20 + 1 + 33 chars.
  169. func (a *Action) ShortRepoPath() string {
  170. return path.Join(a.ShortRepoUserName(), a.ShortRepoName())
  171. }
  172. // GetRepoLink returns relative link to action repository.
  173. func (a *Action) GetRepoLink() string {
  174. // path.Join will skip empty strings
  175. return path.Join(setting.AppSubURL, "/", url.PathEscape(a.GetRepoUserName()), url.PathEscape(a.GetRepoName()))
  176. }
  177. // GetRepositoryFromMatch returns a *repo_model.Repository from a username and repo strings
  178. func GetRepositoryFromMatch(ownerName, repoName string) (*repo_model.Repository, error) {
  179. var err error
  180. refRepo, err := repo_model.GetRepositoryByOwnerAndName(ownerName, repoName)
  181. if err != nil {
  182. if repo_model.IsErrRepoNotExist(err) {
  183. log.Warn("Repository referenced in commit but does not exist: %v", err)
  184. return nil, err
  185. }
  186. log.Error("repo_model.GetRepositoryByOwnerAndName: %v", err)
  187. return nil, err
  188. }
  189. return refRepo, nil
  190. }
  191. // GetCommentLink returns link to action comment.
  192. func (a *Action) GetCommentLink() string {
  193. return a.getCommentLink(db.DefaultContext)
  194. }
  195. func (a *Action) getCommentLink(ctx context.Context) string {
  196. if a == nil {
  197. return "#"
  198. }
  199. if a.Comment == nil && a.CommentID != 0 {
  200. a.Comment, _ = GetCommentByID(ctx, a.CommentID)
  201. }
  202. if a.Comment != nil {
  203. return a.Comment.HTMLURL()
  204. }
  205. if len(a.GetIssueInfos()) == 0 {
  206. return "#"
  207. }
  208. // Return link to issue
  209. issueIDString := a.GetIssueInfos()[0]
  210. issueID, err := strconv.ParseInt(issueIDString, 10, 64)
  211. if err != nil {
  212. return "#"
  213. }
  214. issue, err := getIssueByID(ctx, issueID)
  215. if err != nil {
  216. return "#"
  217. }
  218. if err = issue.LoadRepo(ctx); err != nil {
  219. return "#"
  220. }
  221. return issue.HTMLURL()
  222. }
  223. // GetBranch returns the action's repository branch.
  224. func (a *Action) GetBranch() string {
  225. return strings.TrimPrefix(a.RefName, git.BranchPrefix)
  226. }
  227. // GetRefLink returns the action's ref link.
  228. func (a *Action) GetRefLink() string {
  229. switch {
  230. case strings.HasPrefix(a.RefName, git.BranchPrefix):
  231. return a.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.BranchPrefix))
  232. case strings.HasPrefix(a.RefName, git.TagPrefix):
  233. return a.GetRepoLink() + "/src/tag/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.TagPrefix))
  234. case len(a.RefName) == 40 && git.SHAPattern.MatchString(a.RefName):
  235. return a.GetRepoLink() + "/src/commit/" + a.RefName
  236. default:
  237. // FIXME: we will just assume it's a branch - this was the old way - at some point we may want to enforce that there is always a ref here.
  238. return a.GetRepoLink() + "/src/branch/" + util.PathEscapeSegments(strings.TrimPrefix(a.RefName, git.BranchPrefix))
  239. }
  240. }
  241. // GetTag returns the action's repository tag.
  242. func (a *Action) GetTag() string {
  243. return strings.TrimPrefix(a.RefName, git.TagPrefix)
  244. }
  245. // GetContent returns the action's content.
  246. func (a *Action) GetContent() string {
  247. return a.Content
  248. }
  249. // GetCreate returns the action creation time.
  250. func (a *Action) GetCreate() time.Time {
  251. return a.CreatedUnix.AsTime()
  252. }
  253. // GetIssueInfos returns a list of issues associated with
  254. // the action.
  255. func (a *Action) GetIssueInfos() []string {
  256. return strings.SplitN(a.Content, "|", 3)
  257. }
  258. // GetIssueTitle returns the title of first issue associated
  259. // with the action.
  260. func (a *Action) GetIssueTitle() string {
  261. index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
  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.Title
  268. }
  269. // GetIssueContent returns the content of first issue associated with
  270. // this action.
  271. func (a *Action) GetIssueContent() string {
  272. index, _ := strconv.ParseInt(a.GetIssueInfos()[0], 10, 64)
  273. issue, err := GetIssueByIndex(a.RepoID, index)
  274. if err != nil {
  275. log.Error("GetIssueByIndex: %v", err)
  276. return "500 when get issue"
  277. }
  278. return issue.Content
  279. }
  280. // GetFeedsOptions options for retrieving feeds
  281. type GetFeedsOptions struct {
  282. db.ListOptions
  283. RequestedUser *user_model.User // the user we want activity for
  284. RequestedTeam *organization.Team // the team we want activity for
  285. RequestedRepo *repo_model.Repository // the repo we want activity for
  286. Actor *user_model.User // the user viewing the activity
  287. IncludePrivate bool // include private actions
  288. OnlyPerformedBy bool // only actions performed by requested user
  289. IncludeDeleted bool // include deleted actions
  290. Date string // the day we want activity for: YYYY-MM-DD
  291. }
  292. // GetFeeds returns actions according to the provided options
  293. func GetFeeds(ctx context.Context, opts GetFeedsOptions) (ActionList, error) {
  294. if opts.RequestedUser == nil && opts.RequestedTeam == nil && opts.RequestedRepo == nil {
  295. return nil, fmt.Errorf("need at least one of these filters: RequestedUser, RequestedTeam, RequestedRepo")
  296. }
  297. cond, err := activityQueryCondition(opts)
  298. if err != nil {
  299. return nil, err
  300. }
  301. sess := db.GetEngine(ctx).Where(cond).
  302. Select("`action`.*"). // this line will avoid select other joined table's columns
  303. Join("INNER", "repository", "`repository`.id = `action`.repo_id")
  304. opts.SetDefaultValues()
  305. sess = db.SetSessionPagination(sess, &opts)
  306. actions := make([]*Action, 0, opts.PageSize)
  307. if err := sess.Desc("`action`.created_unix").Find(&actions); err != nil {
  308. return nil, fmt.Errorf("Find: %v", err)
  309. }
  310. if err := ActionList(actions).loadAttributes(ctx); err != nil {
  311. return nil, fmt.Errorf("LoadAttributes: %v", err)
  312. }
  313. return actions, nil
  314. }
  315. func activityReadable(user, doer *user_model.User) bool {
  316. return !user.KeepActivityPrivate ||
  317. doer != nil && (doer.IsAdmin || user.ID == doer.ID)
  318. }
  319. func activityQueryCondition(opts GetFeedsOptions) (builder.Cond, error) {
  320. cond := builder.NewCond()
  321. if opts.RequestedTeam != nil && opts.RequestedUser == nil {
  322. org, err := user_model.GetUserByID(opts.RequestedTeam.OrgID)
  323. if err != nil {
  324. return nil, err
  325. }
  326. opts.RequestedUser = org
  327. }
  328. // check activity visibility for actor ( similar to activityReadable() )
  329. if opts.Actor == nil {
  330. cond = cond.And(builder.In("act_user_id",
  331. builder.Select("`user`.id").Where(
  332. builder.Eq{"keep_activity_private": false, "visibility": structs.VisibleTypePublic},
  333. ).From("`user`"),
  334. ))
  335. } else if !opts.Actor.IsAdmin {
  336. cond = cond.And(builder.In("act_user_id",
  337. builder.Select("`user`.id").Where(
  338. builder.Eq{"keep_activity_private": false}.
  339. And(builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))).
  340. Or(builder.Eq{"id": opts.Actor.ID}).From("`user`"),
  341. ))
  342. }
  343. // check readable repositories by doer/actor
  344. if opts.Actor == nil || !opts.Actor.IsAdmin {
  345. cond = cond.And(builder.In("repo_id", AccessibleRepoIDsQuery(opts.Actor)))
  346. }
  347. if opts.RequestedRepo != nil {
  348. cond = cond.And(builder.Eq{"repo_id": opts.RequestedRepo.ID})
  349. }
  350. if opts.RequestedTeam != nil {
  351. env := organization.OrgFromUser(opts.RequestedUser).AccessibleTeamReposEnv(opts.RequestedTeam)
  352. teamRepoIDs, err := env.RepoIDs(1, opts.RequestedUser.NumRepos)
  353. if err != nil {
  354. return nil, fmt.Errorf("GetTeamRepositories: %v", err)
  355. }
  356. cond = cond.And(builder.In("repo_id", teamRepoIDs))
  357. }
  358. if opts.RequestedUser != nil {
  359. cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
  360. if opts.OnlyPerformedBy {
  361. cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID})
  362. }
  363. }
  364. if !opts.IncludePrivate {
  365. cond = cond.And(builder.Eq{"`action`.is_private": false})
  366. }
  367. if !opts.IncludeDeleted {
  368. cond = cond.And(builder.Eq{"is_deleted": false})
  369. }
  370. if opts.Date != "" {
  371. dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation)
  372. if err != nil {
  373. log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err)
  374. } else {
  375. dateHigh := dateLow.Add(86399000000000) // 23h59m59s
  376. cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()})
  377. cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()})
  378. }
  379. }
  380. return cond, nil
  381. }
  382. // DeleteOldActions deletes all old actions from database.
  383. func DeleteOldActions(olderThan time.Duration) (err error) {
  384. if olderThan <= 0 {
  385. return nil
  386. }
  387. _, err = db.GetEngine(db.DefaultContext).Where("created_unix < ?", time.Now().Add(-olderThan).Unix()).Delete(&Action{})
  388. return
  389. }
  390. func notifyWatchers(ctx context.Context, actions ...*Action) error {
  391. var watchers []*repo_model.Watch
  392. var repo *repo_model.Repository
  393. var err error
  394. var permCode []bool
  395. var permIssue []bool
  396. var permPR []bool
  397. e := db.GetEngine(ctx)
  398. for _, act := range actions {
  399. repoChanged := repo == nil || repo.ID != act.RepoID
  400. if repoChanged {
  401. // Add feeds for user self and all watchers.
  402. watchers, err = repo_model.GetWatchers(ctx, act.RepoID)
  403. if err != nil {
  404. return fmt.Errorf("get watchers: %v", err)
  405. }
  406. }
  407. // Add feed for actioner.
  408. act.UserID = act.ActUserID
  409. if _, err = e.Insert(act); err != nil {
  410. return fmt.Errorf("insert new actioner: %v", err)
  411. }
  412. if repoChanged {
  413. act.loadRepo()
  414. repo = act.Repo
  415. // check repo owner exist.
  416. if err := act.Repo.GetOwner(ctx); err != nil {
  417. return fmt.Errorf("can't get repo owner: %v", err)
  418. }
  419. } else if act.Repo == nil {
  420. act.Repo = repo
  421. }
  422. // Add feed for organization
  423. if act.Repo.Owner.IsOrganization() && act.ActUserID != act.Repo.Owner.ID {
  424. act.ID = 0
  425. act.UserID = act.Repo.Owner.ID
  426. if _, err = e.InsertOne(act); err != nil {
  427. return fmt.Errorf("insert new actioner: %v", err)
  428. }
  429. }
  430. if repoChanged {
  431. permCode = make([]bool, len(watchers))
  432. permIssue = make([]bool, len(watchers))
  433. permPR = make([]bool, len(watchers))
  434. for i, watcher := range watchers {
  435. user, err := user_model.GetUserByIDCtx(ctx, watcher.UserID)
  436. if err != nil {
  437. permCode[i] = false
  438. permIssue[i] = false
  439. permPR[i] = false
  440. continue
  441. }
  442. perm, err := access_model.GetUserRepoPermission(ctx, repo, user)
  443. if err != nil {
  444. permCode[i] = false
  445. permIssue[i] = false
  446. permPR[i] = false
  447. continue
  448. }
  449. permCode[i] = perm.CanRead(unit.TypeCode)
  450. permIssue[i] = perm.CanRead(unit.TypeIssues)
  451. permPR[i] = perm.CanRead(unit.TypePullRequests)
  452. }
  453. }
  454. for i, watcher := range watchers {
  455. if act.ActUserID == watcher.UserID {
  456. continue
  457. }
  458. act.ID = 0
  459. act.UserID = watcher.UserID
  460. act.Repo.Units = nil
  461. switch act.OpType {
  462. case ActionCommitRepo, ActionPushTag, ActionDeleteTag, ActionPublishRelease, ActionDeleteBranch:
  463. if !permCode[i] {
  464. continue
  465. }
  466. case ActionCreateIssue, ActionCommentIssue, ActionCloseIssue, ActionReopenIssue:
  467. if !permIssue[i] {
  468. continue
  469. }
  470. case ActionCreatePullRequest, ActionCommentPull, ActionMergePullRequest, ActionClosePullRequest, ActionReopenPullRequest:
  471. if !permPR[i] {
  472. continue
  473. }
  474. }
  475. if _, err = e.InsertOne(act); err != nil {
  476. return fmt.Errorf("insert new action: %v", err)
  477. }
  478. }
  479. }
  480. return nil
  481. }
  482. // NotifyWatchers creates batch of actions for every watcher.
  483. func NotifyWatchers(actions ...*Action) error {
  484. return notifyWatchers(db.DefaultContext, actions...)
  485. }
  486. // NotifyWatchersActions creates batch of actions for every watcher.
  487. func NotifyWatchersActions(acts []*Action) error {
  488. ctx, committer, err := db.TxContext()
  489. if err != nil {
  490. return err
  491. }
  492. defer committer.Close()
  493. for _, act := range acts {
  494. if err := notifyWatchers(ctx, act); err != nil {
  495. return err
  496. }
  497. }
  498. return committer.Commit()
  499. }