gitea源码

action.go 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // SPDX-License-Identifier: MIT
  4. package activities
  5. import (
  6. "context"
  7. "fmt"
  8. "net/url"
  9. "path"
  10. "slices"
  11. "strconv"
  12. "strings"
  13. "time"
  14. "code.gitea.io/gitea/models/db"
  15. issues_model "code.gitea.io/gitea/models/issues"
  16. "code.gitea.io/gitea/models/organization"
  17. repo_model "code.gitea.io/gitea/models/repo"
  18. user_model "code.gitea.io/gitea/models/user"
  19. "code.gitea.io/gitea/modules/git"
  20. "code.gitea.io/gitea/modules/log"
  21. "code.gitea.io/gitea/modules/setting"
  22. "code.gitea.io/gitea/modules/structs"
  23. "code.gitea.io/gitea/modules/timeutil"
  24. "code.gitea.io/gitea/modules/util"
  25. "xorm.io/builder"
  26. "xorm.io/xorm/schemas"
  27. )
  28. // ActionType represents the type of an action.
  29. type ActionType int
  30. // Possible action types.
  31. const (
  32. ActionCreateRepo ActionType = iota + 1 // 1
  33. ActionRenameRepo // 2
  34. ActionStarRepo // 3
  35. ActionWatchRepo // 4
  36. ActionCommitRepo // 5
  37. ActionCreateIssue // 6
  38. ActionCreatePullRequest // 7
  39. ActionTransferRepo // 8
  40. ActionPushTag // 9
  41. ActionCommentIssue // 10
  42. ActionMergePullRequest // 11
  43. ActionCloseIssue // 12
  44. ActionReopenIssue // 13
  45. ActionClosePullRequest // 14
  46. ActionReopenPullRequest // 15
  47. ActionDeleteTag // 16
  48. ActionDeleteBranch // 17
  49. ActionMirrorSyncPush // 18
  50. ActionMirrorSyncCreate // 19
  51. ActionMirrorSyncDelete // 20
  52. ActionApprovePullRequest // 21
  53. ActionRejectPullRequest // 22
  54. ActionCommentPull // 23
  55. ActionPublishRelease // 24
  56. ActionPullReviewDismissed // 25
  57. ActionPullRequestReadyForReview // 26
  58. ActionAutoMergePullRequest // 27
  59. )
  60. func (at ActionType) String() string {
  61. switch at {
  62. case ActionCreateRepo:
  63. return "create_repo"
  64. case ActionRenameRepo:
  65. return "rename_repo"
  66. case ActionStarRepo:
  67. return "star_repo" // will not displayed in feeds.tmpl
  68. case ActionWatchRepo:
  69. return "watch_repo" // will not displayed in feeds.tmpl
  70. case ActionCommitRepo:
  71. return "commit_repo"
  72. case ActionCreateIssue:
  73. return "create_issue"
  74. case ActionCreatePullRequest:
  75. return "create_pull_request"
  76. case ActionTransferRepo:
  77. return "transfer_repo"
  78. case ActionPushTag:
  79. return "push_tag"
  80. case ActionCommentIssue:
  81. return "comment_issue"
  82. case ActionMergePullRequest:
  83. return "merge_pull_request"
  84. case ActionCloseIssue:
  85. return "close_issue"
  86. case ActionReopenIssue:
  87. return "reopen_issue"
  88. case ActionClosePullRequest:
  89. return "close_pull_request"
  90. case ActionReopenPullRequest:
  91. return "reopen_pull_request"
  92. case ActionDeleteTag:
  93. return "delete_tag"
  94. case ActionDeleteBranch:
  95. return "delete_branch"
  96. case ActionMirrorSyncPush:
  97. return "mirror_sync_push"
  98. case ActionMirrorSyncCreate:
  99. return "mirror_sync_create"
  100. case ActionMirrorSyncDelete:
  101. return "mirror_sync_delete"
  102. case ActionApprovePullRequest:
  103. return "approve_pull_request"
  104. case ActionRejectPullRequest:
  105. return "reject_pull_request"
  106. case ActionCommentPull:
  107. return "comment_pull"
  108. case ActionPublishRelease:
  109. return "publish_release"
  110. case ActionPullReviewDismissed:
  111. return "pull_review_dismissed"
  112. case ActionPullRequestReadyForReview:
  113. return "pull_request_ready_for_review"
  114. case ActionAutoMergePullRequest:
  115. return "auto_merge_pull_request"
  116. default:
  117. return "action-" + strconv.Itoa(int(at))
  118. }
  119. }
  120. func (at ActionType) InActions(actions ...string) bool {
  121. return slices.Contains(actions, at.String())
  122. }
  123. // Action represents user operation type and other information to
  124. // repository. It implemented interface base.Actioner so that can be
  125. // used in template render.
  126. type Action struct {
  127. ID int64 `xorm:"pk autoincr"`
  128. UserID int64 `xorm:"INDEX"` // Receiver user id.
  129. OpType ActionType
  130. ActUserID int64 // Action user id.
  131. ActUser *user_model.User `xorm:"-"`
  132. RepoID int64
  133. Repo *repo_model.Repository `xorm:"-"`
  134. CommentID int64 `xorm:"INDEX"`
  135. Comment *issues_model.Comment `xorm:"-"`
  136. Issue *issues_model.Issue `xorm:"-"` // get the issue id from content
  137. IsDeleted bool `xorm:"NOT NULL DEFAULT false"`
  138. RefName string
  139. IsPrivate bool `xorm:"NOT NULL DEFAULT false"`
  140. Content string `xorm:"TEXT"`
  141. CreatedUnix timeutil.TimeStamp `xorm:"created"`
  142. }
  143. func init() {
  144. db.RegisterModel(new(Action))
  145. }
  146. // TableIndices implements xorm's TableIndices interface
  147. func (a *Action) TableIndices() []*schemas.Index {
  148. repoIndex := schemas.NewIndex("r_u_d", schemas.IndexType)
  149. repoIndex.AddColumn("repo_id", "user_id", "is_deleted")
  150. actUserIndex := schemas.NewIndex("au_r_c_u_d", schemas.IndexType)
  151. actUserIndex.AddColumn("act_user_id", "repo_id", "created_unix", "user_id", "is_deleted")
  152. cudIndex := schemas.NewIndex("c_u_d", schemas.IndexType)
  153. cudIndex.AddColumn("created_unix", "user_id", "is_deleted")
  154. cuIndex := schemas.NewIndex("c_u", schemas.IndexType)
  155. cuIndex.AddColumn("user_id", "is_deleted")
  156. actUserUserIndex := schemas.NewIndex("au_c_u", schemas.IndexType)
  157. actUserUserIndex.AddColumn("act_user_id", "created_unix", "user_id")
  158. indices := []*schemas.Index{actUserIndex, repoIndex, cudIndex, cuIndex, actUserUserIndex}
  159. return indices
  160. }
  161. // GetOpType gets the ActionType of this action.
  162. func (a *Action) GetOpType() ActionType {
  163. return a.OpType
  164. }
  165. // LoadActUser loads a.ActUser
  166. func (a *Action) LoadActUser(ctx context.Context) {
  167. if a.ActUser != nil {
  168. return
  169. }
  170. var err error
  171. a.ActUser, err = user_model.GetPossibleUserByID(ctx, a.ActUserID)
  172. if err == nil {
  173. return
  174. } else if user_model.IsErrUserNotExist(err) {
  175. a.ActUser = user_model.NewGhostUser()
  176. } else {
  177. log.Error("GetUserByID(%d): %v", a.ActUserID, err)
  178. }
  179. }
  180. func (a *Action) LoadRepo(ctx context.Context) error {
  181. if a.Repo != nil {
  182. return nil
  183. }
  184. var err error
  185. a.Repo, err = repo_model.GetRepositoryByID(ctx, a.RepoID)
  186. return err
  187. }
  188. // GetActFullName gets the action's user full name.
  189. func (a *Action) GetActFullName(ctx context.Context) string {
  190. a.LoadActUser(ctx)
  191. return a.ActUser.FullName
  192. }
  193. // GetActUserName gets the action's user name.
  194. func (a *Action) GetActUserName(ctx context.Context) string {
  195. a.LoadActUser(ctx)
  196. return a.ActUser.Name
  197. }
  198. // ShortActUserName gets the action's user name trimmed to max 20
  199. // chars.
  200. func (a *Action) ShortActUserName(ctx context.Context) string {
  201. return util.EllipsisDisplayString(a.GetActUserName(ctx), 20)
  202. }
  203. // GetActDisplayName gets the action's display name based on DEFAULT_SHOW_FULL_NAME, or falls back to the username if it is blank.
  204. func (a *Action) GetActDisplayName(ctx context.Context) string {
  205. if setting.UI.DefaultShowFullName {
  206. trimmedFullName := strings.TrimSpace(a.GetActFullName(ctx))
  207. if len(trimmedFullName) > 0 {
  208. return trimmedFullName
  209. }
  210. }
  211. return a.ShortActUserName(ctx)
  212. }
  213. // GetActDisplayNameTitle gets the action's display name used for the title (tooltip) based on DEFAULT_SHOW_FULL_NAME
  214. func (a *Action) GetActDisplayNameTitle(ctx context.Context) string {
  215. if setting.UI.DefaultShowFullName {
  216. return a.ShortActUserName(ctx)
  217. }
  218. return a.GetActFullName(ctx)
  219. }
  220. // GetRepoUserName returns the name of the action repository owner.
  221. func (a *Action) GetRepoUserName(ctx context.Context) string {
  222. _ = a.LoadRepo(ctx)
  223. if a.Repo == nil {
  224. return "(non-existing-repo)"
  225. }
  226. return a.Repo.OwnerName
  227. }
  228. // ShortRepoUserName returns the name of the action repository owner
  229. // trimmed to max 20 chars.
  230. func (a *Action) ShortRepoUserName(ctx context.Context) string {
  231. return util.EllipsisDisplayString(a.GetRepoUserName(ctx), 20)
  232. }
  233. // GetRepoName returns the name of the action repository.
  234. func (a *Action) GetRepoName(ctx context.Context) string {
  235. _ = a.LoadRepo(ctx)
  236. if a.Repo == nil {
  237. return "(non-existing-repo)"
  238. }
  239. return a.Repo.Name
  240. }
  241. // ShortRepoName returns the name of the action repository
  242. // trimmed to max 33 chars.
  243. func (a *Action) ShortRepoName(ctx context.Context) string {
  244. return util.EllipsisDisplayString(a.GetRepoName(ctx), 33)
  245. }
  246. // GetRepoPath returns the virtual path to the action repository.
  247. func (a *Action) GetRepoPath(ctx context.Context) string {
  248. return path.Join(a.GetRepoUserName(ctx), a.GetRepoName(ctx))
  249. }
  250. // ShortRepoPath returns the virtual path to the action repository
  251. // trimmed to max 20 + 1 + 33 chars.
  252. func (a *Action) ShortRepoPath(ctx context.Context) string {
  253. return path.Join(a.ShortRepoUserName(ctx), a.ShortRepoName(ctx))
  254. }
  255. // GetRepoLink returns relative link to action repository.
  256. func (a *Action) GetRepoLink(ctx context.Context) string {
  257. // path.Join will skip empty strings
  258. return path.Join(setting.AppSubURL, "/", url.PathEscape(a.GetRepoUserName(ctx)), url.PathEscape(a.GetRepoName(ctx)))
  259. }
  260. // GetRepoAbsoluteLink returns the absolute link to action repository.
  261. func (a *Action) GetRepoAbsoluteLink(ctx context.Context) string {
  262. return setting.AppURL + url.PathEscape(a.GetRepoUserName(ctx)) + "/" + url.PathEscape(a.GetRepoName(ctx))
  263. }
  264. func (a *Action) loadComment(ctx context.Context) (err error) {
  265. if a.CommentID == 0 || a.Comment != nil {
  266. return nil
  267. }
  268. a.Comment, err = issues_model.GetCommentByID(ctx, a.CommentID)
  269. return err
  270. }
  271. // GetCommentHTMLURL returns link to action comment.
  272. func (a *Action) GetCommentHTMLURL(ctx context.Context) string {
  273. if a == nil {
  274. return "#"
  275. }
  276. _ = a.loadComment(ctx)
  277. if a.Comment != nil {
  278. return a.Comment.HTMLURL(ctx)
  279. }
  280. if err := a.LoadIssue(ctx); err != nil || a.Issue == nil {
  281. return "#"
  282. }
  283. if err := a.Issue.LoadRepo(ctx); err != nil {
  284. return "#"
  285. }
  286. return a.Issue.HTMLURL(ctx)
  287. }
  288. // GetCommentLink returns link to action comment.
  289. func (a *Action) GetCommentLink(ctx context.Context) string {
  290. if a == nil {
  291. return "#"
  292. }
  293. _ = a.loadComment(ctx)
  294. if a.Comment != nil {
  295. return a.Comment.Link(ctx)
  296. }
  297. if err := a.LoadIssue(ctx); err != nil || a.Issue == nil {
  298. return "#"
  299. }
  300. if err := a.Issue.LoadRepo(ctx); err != nil {
  301. return "#"
  302. }
  303. return a.Issue.Link()
  304. }
  305. // GetBranch returns the action's repository branch.
  306. func (a *Action) GetBranch() string {
  307. return strings.TrimPrefix(a.RefName, git.BranchPrefix)
  308. }
  309. // GetRefLink returns the action's ref link.
  310. func (a *Action) GetRefLink(ctx context.Context) string {
  311. return a.GetRepoLink(ctx) + "/src/" + git.RefName(a.RefName).RefWebLinkPath()
  312. }
  313. // GetTag returns the action's repository tag.
  314. func (a *Action) GetTag() string {
  315. return strings.TrimPrefix(a.RefName, git.TagPrefix)
  316. }
  317. // GetContent returns the action's content.
  318. func (a *Action) GetContent() string {
  319. return a.Content
  320. }
  321. // GetCreate returns the action creation time.
  322. func (a *Action) GetCreate() time.Time {
  323. return a.CreatedUnix.AsTime()
  324. }
  325. func (a *Action) IsIssueEvent() bool {
  326. return a.OpType.InActions("comment_issue", "approve_pull_request", "reject_pull_request", "comment_pull", "merge_pull_request")
  327. }
  328. // GetIssueInfos returns a list of associated information with the action.
  329. func (a *Action) GetIssueInfos() []string {
  330. // make sure it always returns 3 elements, because there are some access to the a[1] and a[2] without checking the length
  331. ret := strings.SplitN(a.Content, "|", 3)
  332. for len(ret) < 3 {
  333. ret = append(ret, "")
  334. }
  335. return ret
  336. }
  337. func (a *Action) getIssueIndex() int64 {
  338. infos := a.GetIssueInfos()
  339. if len(infos) == 0 {
  340. return 0
  341. }
  342. index, _ := strconv.ParseInt(infos[0], 10, 64)
  343. return index
  344. }
  345. func (a *Action) LoadIssue(ctx context.Context) error {
  346. if a.Issue != nil {
  347. return nil
  348. }
  349. if index := a.getIssueIndex(); index > 0 {
  350. issue, err := issues_model.GetIssueByIndex(ctx, a.RepoID, index)
  351. if err != nil {
  352. return err
  353. }
  354. a.Issue = issue
  355. a.Issue.Repo = a.Repo
  356. }
  357. return nil
  358. }
  359. // GetIssueTitle returns the title of first issue associated with the action.
  360. func (a *Action) GetIssueTitle(ctx context.Context) string {
  361. if err := a.LoadIssue(ctx); err != nil {
  362. log.Error("LoadIssue: %v", err)
  363. return "<500 when get issue>"
  364. }
  365. if a.Issue == nil {
  366. return "<Issue not found>"
  367. }
  368. return a.Issue.Title
  369. }
  370. // GetIssueContent returns the content of first issue associated with this action.
  371. func (a *Action) GetIssueContent(ctx context.Context) string {
  372. if err := a.LoadIssue(ctx); err != nil {
  373. log.Error("LoadIssue: %v", err)
  374. return "<500 when get issue>"
  375. }
  376. if a.Issue == nil {
  377. return "<Content not found>"
  378. }
  379. return a.Issue.Content
  380. }
  381. // GetFeedsOptions options for retrieving feeds
  382. type GetFeedsOptions struct {
  383. db.ListOptions
  384. RequestedUser *user_model.User // the user we want activity for
  385. RequestedTeam *organization.Team // the team we want activity for
  386. RequestedRepo *repo_model.Repository // the repo we want activity for
  387. Actor *user_model.User // the user viewing the activity
  388. IncludePrivate bool // include private actions
  389. OnlyPerformedBy bool // only actions performed by requested user
  390. IncludeDeleted bool // include deleted actions
  391. Date string // the day we want activity for: YYYY-MM-DD
  392. DontCount bool // do counting in GetFeeds
  393. }
  394. // ActivityReadable return whether doer can read activities of user
  395. func ActivityReadable(user, doer *user_model.User) bool {
  396. return !user.KeepActivityPrivate ||
  397. doer != nil && (doer.IsAdmin || user.ID == doer.ID)
  398. }
  399. func FeedDateCond(opts GetFeedsOptions) builder.Cond {
  400. cond := builder.NewCond()
  401. if opts.Date == "" {
  402. return cond
  403. }
  404. dateLow, err := time.ParseInLocation("2006-01-02", opts.Date, setting.DefaultUILocation)
  405. if err != nil {
  406. log.Warn("Unable to parse %s, filter not applied: %v", opts.Date, err)
  407. } else {
  408. dateHigh := dateLow.Add(86399000000000) // 23h59m59s
  409. cond = cond.And(builder.Gte{"`action`.created_unix": dateLow.Unix()})
  410. cond = cond.And(builder.Lte{"`action`.created_unix": dateHigh.Unix()})
  411. }
  412. return cond
  413. }
  414. func ActivityQueryCondition(ctx context.Context, opts GetFeedsOptions) (builder.Cond, error) {
  415. cond := builder.NewCond()
  416. if opts.RequestedTeam != nil && opts.RequestedUser == nil {
  417. org, err := user_model.GetUserByID(ctx, opts.RequestedTeam.OrgID)
  418. if err != nil {
  419. return nil, err
  420. }
  421. opts.RequestedUser = org
  422. }
  423. // check activity visibility for actor ( similar to activityReadable() )
  424. if opts.Actor == nil {
  425. cond = cond.And(builder.In("act_user_id",
  426. builder.Select("`user`.id").Where(
  427. builder.Eq{"keep_activity_private": false, "visibility": structs.VisibleTypePublic},
  428. ).From("`user`"),
  429. ))
  430. } else if !opts.Actor.IsAdmin {
  431. uidCond := builder.Select("`user`.id").From("`user`").Where(
  432. builder.Eq{"keep_activity_private": false}.
  433. And(builder.In("visibility", structs.VisibleTypePublic, structs.VisibleTypeLimited))).
  434. Or(builder.Eq{"id": opts.Actor.ID})
  435. if opts.RequestedUser != nil {
  436. if opts.RequestedUser.IsOrganization() {
  437. // An organization can always see the activities whose `act_user_id` is the same as its id.
  438. uidCond = uidCond.Or(builder.Eq{"id": opts.RequestedUser.ID})
  439. } else {
  440. // A user can always see the activities of the organizations to which the user belongs.
  441. uidCond = uidCond.Or(
  442. builder.Eq{"type": user_model.UserTypeOrganization}.
  443. And(builder.In("`user`.id", builder.Select("org_id").
  444. Where(builder.Eq{"uid": opts.RequestedUser.ID}).
  445. From("team_user"))),
  446. )
  447. }
  448. }
  449. cond = cond.And(builder.In("act_user_id", uidCond))
  450. }
  451. // check readable repositories by doer/actor
  452. if opts.Actor == nil || !opts.Actor.IsAdmin {
  453. cond = cond.And(builder.In("repo_id", repo_model.AccessibleRepoIDsQuery(opts.Actor)))
  454. }
  455. if opts.RequestedRepo != nil {
  456. // repo's actions could have duplicate items, see the comment of NotifyWatchers
  457. // so here we only filter the "original items", aka: user_id == act_user_id
  458. cond = cond.And(
  459. builder.Eq{"`action`.repo_id": opts.RequestedRepo.ID},
  460. builder.Expr("`action`.user_id = `action`.act_user_id"),
  461. )
  462. }
  463. if opts.RequestedTeam != nil {
  464. env := repo_model.AccessibleTeamReposEnv(organization.OrgFromUser(opts.RequestedUser), opts.RequestedTeam)
  465. teamRepoIDs, err := env.RepoIDs(ctx)
  466. if err != nil {
  467. return nil, fmt.Errorf("GetTeamRepositories: %w", err)
  468. }
  469. cond = cond.And(builder.In("repo_id", teamRepoIDs))
  470. }
  471. if opts.RequestedUser != nil {
  472. cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
  473. if opts.OnlyPerformedBy {
  474. cond = cond.And(builder.Eq{"act_user_id": opts.RequestedUser.ID})
  475. }
  476. }
  477. if !opts.IncludePrivate {
  478. cond = cond.And(builder.Eq{"`action`.is_private": false})
  479. }
  480. if !opts.IncludeDeleted {
  481. cond = cond.And(builder.Eq{"is_deleted": false})
  482. }
  483. cond = cond.And(FeedDateCond(opts))
  484. return cond, nil
  485. }
  486. // DeleteOldActions deletes all old actions from database.
  487. func DeleteOldActions(ctx context.Context, olderThan time.Duration) (err error) {
  488. if olderThan <= 0 {
  489. return nil
  490. }
  491. _, err = db.GetEngine(ctx).Where("created_unix < ?", time.Now().Add(-olderThan).Unix()).Delete(&Action{})
  492. return err
  493. }
  494. // DeleteIssueActions delete all actions related with issueID
  495. func DeleteIssueActions(ctx context.Context, repoID, issueID, issueIndex int64) error {
  496. // delete actions assigned to this issue
  497. e := db.GetEngine(ctx)
  498. // MariaDB has a performance bug: https://jira.mariadb.org/browse/MDEV-16289
  499. // so here it uses "DELETE ... WHERE IN" with pre-queried IDs.
  500. var lastCommentID int64
  501. commentIDs := make([]int64, 0, db.DefaultMaxInSize)
  502. for {
  503. commentIDs = commentIDs[:0]
  504. err := e.Select("`id`").Table(&issues_model.Comment{}).
  505. Where(builder.Eq{"issue_id": issueID}).And("`id` > ?", lastCommentID).
  506. OrderBy("`id`").Limit(db.DefaultMaxInSize).
  507. Find(&commentIDs)
  508. if err != nil {
  509. return err
  510. } else if len(commentIDs) == 0 {
  511. break
  512. } else if _, err = db.GetEngine(ctx).In("comment_id", commentIDs).Delete(&Action{}); err != nil {
  513. return err
  514. }
  515. lastCommentID = commentIDs[len(commentIDs)-1]
  516. }
  517. _, err := e.Where("repo_id = ?", repoID).
  518. In("op_type", ActionCreateIssue, ActionCreatePullRequest).
  519. Where("content LIKE ?", strconv.FormatInt(issueIndex, 10)+"|%"). // "IssueIndex|content..."
  520. Delete(&Action{})
  521. return err
  522. }
  523. // CountActionCreatedUnixString count actions where created_unix is an empty string
  524. func CountActionCreatedUnixString(ctx context.Context) (int64, error) {
  525. if setting.Database.Type.IsSQLite3() {
  526. return db.GetEngine(ctx).Where(`created_unix = ''`).Count(new(Action))
  527. }
  528. return 0, nil
  529. }
  530. // FixActionCreatedUnixString set created_unix to zero if it is an empty string
  531. func FixActionCreatedUnixString(ctx context.Context) (int64, error) {
  532. if setting.Database.Type.IsSQLite3() {
  533. res, err := db.GetEngine(ctx).Exec(`UPDATE action SET created_unix = 0 WHERE created_unix = ''`)
  534. if err != nil {
  535. return 0, err
  536. }
  537. return res.RowsAffected()
  538. }
  539. return 0, nil
  540. }