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.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. // Copyright 2021 The Gitea 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 issues
  5. import (
  6. "context"
  7. "fmt"
  8. "code.gitea.io/gitea/models/avatars"
  9. "code.gitea.io/gitea/models/db"
  10. "code.gitea.io/gitea/modules/log"
  11. "code.gitea.io/gitea/modules/timeutil"
  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. item.UserAvatarLink = avatars.GenerateUserAvatarFastLink(item.UserName, 0)
  146. }
  147. return res, nil
  148. }
  149. // HasIssueContentHistory check if a ContentHistory entry exists
  150. func HasIssueContentHistory(dbCtx context.Context, issueID, commentID int64) (bool, error) {
  151. exists, err := db.GetEngine(dbCtx).Cols("id").Exist(&ContentHistory{
  152. IssueID: issueID,
  153. CommentID: commentID,
  154. })
  155. if err != nil {
  156. log.Error("can not fetch issue content history. err=%v", err)
  157. return false, err
  158. }
  159. return exists, err
  160. }
  161. // SoftDeleteIssueContentHistory soft delete
  162. func SoftDeleteIssueContentHistory(dbCtx context.Context, historyID int64) error {
  163. if _, err := db.GetEngine(dbCtx).ID(historyID).Cols("is_deleted", "content_text").Update(&ContentHistory{
  164. IsDeleted: true,
  165. ContentText: "",
  166. }); err != nil {
  167. log.Error("failed to soft delete issue content history. err=%v", err)
  168. return err
  169. }
  170. return nil
  171. }
  172. // ErrIssueContentHistoryNotExist not exist error
  173. type ErrIssueContentHistoryNotExist struct {
  174. ID int64
  175. }
  176. // Error error string
  177. func (err ErrIssueContentHistoryNotExist) Error() string {
  178. return fmt.Sprintf("issue content history does not exist [id: %d]", err.ID)
  179. }
  180. // GetIssueContentHistoryByID get issue content history
  181. func GetIssueContentHistoryByID(dbCtx context.Context, id int64) (*ContentHistory, error) {
  182. h := &ContentHistory{}
  183. has, err := db.GetEngine(dbCtx).ID(id).Get(h)
  184. if err != nil {
  185. return nil, err
  186. } else if !has {
  187. return nil, ErrIssueContentHistoryNotExist{id}
  188. }
  189. return h, nil
  190. }
  191. // GetIssueContentHistoryAndPrev get a history and the previous non-deleted history (to compare)
  192. func GetIssueContentHistoryAndPrev(dbCtx context.Context, id int64) (history, prevHistory *ContentHistory, err error) {
  193. history = &ContentHistory{}
  194. has, err := db.GetEngine(dbCtx).ID(id).Get(history)
  195. if err != nil {
  196. log.Error("failed to get issue content history %v. err=%v", id, err)
  197. return nil, nil, err
  198. } else if !has {
  199. log.Error("issue content history does not exist. id=%v. err=%v", id, err)
  200. return nil, nil, &ErrIssueContentHistoryNotExist{id}
  201. }
  202. prevHistory = &ContentHistory{}
  203. has, err = db.GetEngine(dbCtx).Where(builder.Eq{"issue_id": history.IssueID, "comment_id": history.CommentID, "is_deleted": false}).
  204. And(builder.Lt{"edited_unix": history.EditedUnix}).
  205. OrderBy("edited_unix DESC").Limit(1).
  206. Get(prevHistory)
  207. if err != nil {
  208. log.Error("failed to get issue content history %v. err=%v", id, err)
  209. return nil, nil, err
  210. } else if !has {
  211. return history, nil, nil
  212. }
  213. return history, prevHistory, nil
  214. }