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