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.

content_history.go 7.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. // Copyright 2021 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package issues
  4. import (
  5. "context"
  6. "fmt"
  7. "code.gitea.io/gitea/models/avatars"
  8. "code.gitea.io/gitea/models/db"
  9. "code.gitea.io/gitea/modules/log"
  10. "code.gitea.io/gitea/modules/timeutil"
  11. "code.gitea.io/gitea/modules/util"
  12. "xorm.io/builder"
  13. )
  14. // ContentHistory save issue/comment content history revisions.
  15. type ContentHistory struct {
  16. ID int64 `xorm:"pk autoincr"`
  17. PosterID int64
  18. IssueID int64 `xorm:"INDEX"`
  19. CommentID int64 `xorm:"INDEX"`
  20. EditedUnix timeutil.TimeStamp `xorm:"INDEX"`
  21. ContentText string `xorm:"LONGTEXT"`
  22. IsFirstCreated bool
  23. IsDeleted bool
  24. }
  25. // TableName provides the real table name
  26. func (m *ContentHistory) TableName() string {
  27. return "issue_content_history"
  28. }
  29. func init() {
  30. db.RegisterModel(new(ContentHistory))
  31. }
  32. // SaveIssueContentHistory save history
  33. func SaveIssueContentHistory(ctx context.Context, posterID, issueID, commentID int64, editTime timeutil.TimeStamp, contentText string, isFirstCreated bool) error {
  34. ch := &ContentHistory{
  35. PosterID: posterID,
  36. IssueID: issueID,
  37. CommentID: commentID,
  38. ContentText: contentText,
  39. EditedUnix: editTime,
  40. IsFirstCreated: isFirstCreated,
  41. }
  42. if err := db.Insert(ctx, ch); err != nil {
  43. log.Error("can not save issue content history. err=%v", err)
  44. return err
  45. }
  46. // We only keep at most 20 history revisions now. It is enough in most cases.
  47. // If there is a special requirement to keep more, we can consider introducing a new setting option then, but not now.
  48. KeepLimitedContentHistory(ctx, issueID, commentID, 20)
  49. return nil
  50. }
  51. // KeepLimitedContentHistory keeps at most `limit` history revisions, it will hard delete out-dated revisions, sorting by revision interval
  52. // we can ignore all errors in this function, so we just log them
  53. func KeepLimitedContentHistory(ctx context.Context, issueID, commentID int64, limit int) {
  54. type IDEditTime struct {
  55. ID int64
  56. EditedUnix timeutil.TimeStamp
  57. }
  58. var res []*IDEditTime
  59. err := db.GetEngine(ctx).Select("id, edited_unix").Table("issue_content_history").
  60. Where(builder.Eq{"issue_id": issueID, "comment_id": commentID}).
  61. OrderBy("edited_unix ASC").
  62. Find(&res)
  63. if err != nil {
  64. log.Error("can not query content history for deletion, err=%v", err)
  65. return
  66. }
  67. if len(res) <= 2 {
  68. return
  69. }
  70. outDatedCount := len(res) - limit
  71. for outDatedCount > 0 {
  72. var indexToDelete int
  73. minEditedInterval := -1
  74. // find a history revision with minimal edited interval to delete, the first and the last should never be deleted
  75. for i := 1; i < len(res)-1; i++ {
  76. editedInterval := int(res[i].EditedUnix - res[i-1].EditedUnix)
  77. if minEditedInterval == -1 || editedInterval < minEditedInterval {
  78. minEditedInterval = editedInterval
  79. indexToDelete = i
  80. }
  81. }
  82. if indexToDelete == 0 {
  83. break
  84. }
  85. // hard delete the found one
  86. _, err = db.GetEngine(ctx).Delete(&ContentHistory{ID: res[indexToDelete].ID})
  87. if err != nil {
  88. log.Error("can not delete out-dated content history, err=%v", err)
  89. break
  90. }
  91. res = append(res[:indexToDelete], res[indexToDelete+1:]...)
  92. outDatedCount--
  93. }
  94. }
  95. // QueryIssueContentHistoryEditedCountMap query related history count of each comment (comment_id = 0 means the main issue)
  96. // only return the count map for "edited" (history revision count > 1) issues or comments.
  97. func QueryIssueContentHistoryEditedCountMap(dbCtx context.Context, issueID int64) (map[int64]int, error) {
  98. type HistoryCountRecord struct {
  99. CommentID int64
  100. HistoryCount int
  101. }
  102. records := make([]*HistoryCountRecord, 0)
  103. err := db.GetEngine(dbCtx).Select("comment_id, COUNT(1) as history_count").
  104. Table("issue_content_history").
  105. Where(builder.Eq{"issue_id": issueID}).
  106. GroupBy("comment_id").
  107. Having("count(1) > 1").
  108. Find(&records)
  109. if err != nil {
  110. log.Error("can not query issue content history count map. err=%v", err)
  111. return nil, err
  112. }
  113. res := map[int64]int{}
  114. for _, r := range records {
  115. res[r.CommentID] = r.HistoryCount
  116. }
  117. return res, nil
  118. }
  119. // IssueContentListItem the list for web ui
  120. type IssueContentListItem struct {
  121. UserID int64
  122. UserName string
  123. UserFullName string
  124. UserAvatarLink string
  125. HistoryID int64
  126. EditedUnix timeutil.TimeStamp
  127. IsFirstCreated bool
  128. IsDeleted bool
  129. }
  130. // FetchIssueContentHistoryList fetch list
  131. func FetchIssueContentHistoryList(dbCtx context.Context, issueID, commentID int64) ([]*IssueContentListItem, error) {
  132. res := make([]*IssueContentListItem, 0)
  133. err := db.GetEngine(dbCtx).Select("u.id as user_id, u.name as user_name, u.full_name as user_full_name,"+
  134. "h.id as history_id, h.edited_unix, h.is_first_created, h.is_deleted").
  135. Table([]string{"issue_content_history", "h"}).
  136. Join("LEFT", []string{"user", "u"}, "h.poster_id = u.id").
  137. Where(builder.Eq{"issue_id": issueID, "comment_id": commentID}).
  138. OrderBy("edited_unix DESC").
  139. Find(&res)
  140. if err != nil {
  141. log.Error("can not fetch issue content history list. err=%v", err)
  142. return nil, err
  143. }
  144. for _, item := range res {
  145. if item.UserID > 0 {
  146. item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0)
  147. } else {
  148. item.UserAvatarLink = avatars.DefaultAvatarLink()
  149. }
  150. }
  151. return res, nil
  152. }
  153. // HasIssueContentHistory check if a ContentHistory entry exists
  154. func HasIssueContentHistory(dbCtx context.Context, issueID, commentID int64) (bool, error) {
  155. exists, err := db.GetEngine(dbCtx).Cols("id").Exist(&ContentHistory{
  156. IssueID: issueID,
  157. CommentID: commentID,
  158. })
  159. if err != nil {
  160. log.Error("can not fetch issue content history. err=%v", err)
  161. return false, err
  162. }
  163. return exists, err
  164. }
  165. // SoftDeleteIssueContentHistory soft delete
  166. func SoftDeleteIssueContentHistory(dbCtx context.Context, historyID int64) error {
  167. if _, err := db.GetEngine(dbCtx).ID(historyID).Cols("is_deleted", "content_text").Update(&ContentHistory{
  168. IsDeleted: true,
  169. ContentText: "",
  170. }); err != nil {
  171. log.Error("failed to soft delete issue content history. err=%v", err)
  172. return err
  173. }
  174. return nil
  175. }
  176. // ErrIssueContentHistoryNotExist not exist error
  177. type ErrIssueContentHistoryNotExist struct {
  178. ID int64
  179. }
  180. // Error error string
  181. func (err ErrIssueContentHistoryNotExist) Error() string {
  182. return fmt.Sprintf("issue content history does not exist [id: %d]", err.ID)
  183. }
  184. func (err ErrIssueContentHistoryNotExist) Unwrap() error {
  185. return util.ErrNotExist
  186. }
  187. // GetIssueContentHistoryByID get issue content history
  188. func GetIssueContentHistoryByID(dbCtx context.Context, id int64) (*ContentHistory, error) {
  189. h := &ContentHistory{}
  190. has, err := db.GetEngine(dbCtx).ID(id).Get(h)
  191. if err != nil {
  192. return nil, err
  193. } else if !has {
  194. return nil, ErrIssueContentHistoryNotExist{id}
  195. }
  196. return h, nil
  197. }
  198. // GetIssueContentHistoryAndPrev get a history and the previous non-deleted history (to compare)
  199. func GetIssueContentHistoryAndPrev(dbCtx context.Context, issueID, id int64) (history, prevHistory *ContentHistory, err error) {
  200. history = &ContentHistory{}
  201. has, err := db.GetEngine(dbCtx).Where("id=? AND issue_id=?", id, issueID).Get(history)
  202. if err != nil {
  203. log.Error("failed to get issue content history %v. err=%v", id, err)
  204. return nil, nil, err
  205. } else if !has {
  206. log.Error("issue content history does not exist. id=%v. err=%v", id, err)
  207. return nil, nil, &ErrIssueContentHistoryNotExist{id}
  208. }
  209. prevHistory = &ContentHistory{}
  210. has, err = db.GetEngine(dbCtx).Where(builder.Eq{"issue_id": history.IssueID, "comment_id": history.CommentID, "is_deleted": false}).
  211. And(builder.Lt{"edited_unix": history.EditedUnix}).
  212. OrderBy("edited_unix DESC").Limit(1).
  213. Get(prevHistory)
  214. if err != nil {
  215. log.Error("failed to get issue content history %v. err=%v", id, err)
  216. return nil, nil, err
  217. } else if !has {
  218. return history, nil, nil
  219. }
  220. return history, prevHistory, nil
  221. }