2021-10-11 01:40:03 +03:00
// Copyright 2021 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2021-10-11 01:40:03 +03:00
package issues
import (
"context"
"fmt"
"code.gitea.io/gitea/models/avatars"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"
2022-10-18 08:50:37 +03:00
"code.gitea.io/gitea/modules/util"
2021-10-11 01:40:03 +03:00
"xorm.io/builder"
)
// ContentHistory save issue/comment content history revisions.
type ContentHistory struct {
ID int64 ` xorm:"pk autoincr" `
PosterID int64
IssueID int64 ` xorm:"INDEX" `
CommentID int64 ` xorm:"INDEX" `
EditedUnix timeutil . TimeStamp ` xorm:"INDEX" `
ContentText string ` xorm:"LONGTEXT" `
IsFirstCreated bool
IsDeleted bool
}
// TableName provides the real table name
func ( m * ContentHistory ) TableName ( ) string {
return "issue_content_history"
}
func init ( ) {
db . RegisterModel ( new ( ContentHistory ) )
}
// SaveIssueContentHistory save history
2022-05-20 17:08:52 +03:00
func SaveIssueContentHistory ( ctx context . Context , posterID , issueID , commentID int64 , editTime timeutil . TimeStamp , contentText string , isFirstCreated bool ) error {
2021-10-11 01:40:03 +03:00
ch := & ContentHistory {
PosterID : posterID ,
IssueID : issueID ,
CommentID : commentID ,
ContentText : contentText ,
EditedUnix : editTime ,
IsFirstCreated : isFirstCreated ,
}
2022-05-20 17:08:52 +03:00
if err := db . Insert ( ctx , ch ) ; err != nil {
2021-10-11 01:40:03 +03:00
log . Error ( "can not save issue content history. err=%v" , err )
return err
}
// We only keep at most 20 history revisions now. It is enough in most cases.
// If there is a special requirement to keep more, we can consider introducing a new setting option then, but not now.
2022-06-13 12:37:59 +03:00
KeepLimitedContentHistory ( ctx , issueID , commentID , 20 )
2021-10-11 01:40:03 +03:00
return nil
}
2022-06-13 12:37:59 +03:00
// KeepLimitedContentHistory keeps at most `limit` history revisions, it will hard delete out-dated revisions, sorting by revision interval
2021-10-11 01:40:03 +03:00
// we can ignore all errors in this function, so we just log them
2022-06-13 12:37:59 +03:00
func KeepLimitedContentHistory ( ctx context . Context , issueID , commentID int64 , limit int ) {
2021-10-11 01:40:03 +03:00
type IDEditTime struct {
ID int64
EditedUnix timeutil . TimeStamp
}
var res [ ] * IDEditTime
2022-05-20 17:08:52 +03:00
err := db . GetEngine ( ctx ) . Select ( "id, edited_unix" ) . Table ( "issue_content_history" ) .
2021-10-11 01:40:03 +03:00
Where ( builder . Eq { "issue_id" : issueID , "comment_id" : commentID } ) .
OrderBy ( "edited_unix ASC" ) .
Find ( & res )
if err != nil {
log . Error ( "can not query content history for deletion, err=%v" , err )
return
}
2021-11-22 15:20:16 +03:00
if len ( res ) <= 2 {
2021-10-11 01:40:03 +03:00
return
}
outDatedCount := len ( res ) - limit
for outDatedCount > 0 {
var indexToDelete int
minEditedInterval := - 1
2021-11-22 15:20:16 +03:00
// find a history revision with minimal edited interval to delete, the first and the last should never be deleted
for i := 1 ; i < len ( res ) - 1 ; i ++ {
2021-10-11 01:40:03 +03:00
editedInterval := int ( res [ i ] . EditedUnix - res [ i - 1 ] . EditedUnix )
if minEditedInterval == - 1 || editedInterval < minEditedInterval {
minEditedInterval = editedInterval
indexToDelete = i
}
}
if indexToDelete == 0 {
break
}
// hard delete the found one
2022-05-20 17:08:52 +03:00
_ , err = db . GetEngine ( ctx ) . Delete ( & ContentHistory { ID : res [ indexToDelete ] . ID } )
2021-10-11 01:40:03 +03:00
if err != nil {
log . Error ( "can not delete out-dated content history, err=%v" , err )
break
}
res = append ( res [ : indexToDelete ] , res [ indexToDelete + 1 : ] ... )
outDatedCount --
}
}
// QueryIssueContentHistoryEditedCountMap query related history count of each comment (comment_id = 0 means the main issue)
// only return the count map for "edited" (history revision count > 1) issues or comments.
func QueryIssueContentHistoryEditedCountMap ( dbCtx context . Context , issueID int64 ) ( map [ int64 ] int , error ) {
type HistoryCountRecord struct {
CommentID int64
HistoryCount int
}
records := make ( [ ] * HistoryCountRecord , 0 )
err := db . GetEngine ( dbCtx ) . Select ( "comment_id, COUNT(1) as history_count" ) .
Table ( "issue_content_history" ) .
Where ( builder . Eq { "issue_id" : issueID } ) .
GroupBy ( "comment_id" ) .
2021-10-21 13:06:19 +03:00
Having ( "count(1) > 1" ) .
2021-10-11 01:40:03 +03:00
Find ( & records )
if err != nil {
log . Error ( "can not query issue content history count map. err=%v" , err )
return nil , err
}
res := map [ int64 ] int { }
for _ , r := range records {
res [ r . CommentID ] = r . HistoryCount
}
return res , nil
}
// IssueContentListItem the list for web ui
type IssueContentListItem struct {
UserID int64
UserName string
2022-02-20 22:50:11 +03:00
UserFullName string
2021-10-11 01:40:03 +03:00
UserAvatarLink string
HistoryID int64
EditedUnix timeutil . TimeStamp
IsFirstCreated bool
IsDeleted bool
}
// FetchIssueContentHistoryList fetch list
2021-12-20 07:41:31 +03:00
func FetchIssueContentHistoryList ( dbCtx context . Context , issueID , commentID int64 ) ( [ ] * IssueContentListItem , error ) {
2021-10-11 01:40:03 +03:00
res := make ( [ ] * IssueContentListItem , 0 )
2022-02-20 22:50:11 +03:00
err := db . GetEngine ( dbCtx ) . Select ( "u.id as user_id, u.name as user_name, u.full_name as user_full_name," +
2021-10-11 01:40:03 +03:00
"h.id as history_id, h.edited_unix, h.is_first_created, h.is_deleted" ) .
Table ( [ ] string { "issue_content_history" , "h" } ) .
Join ( "LEFT" , [ ] string { "user" , "u" } , "h.poster_id = u.id" ) .
Where ( builder . Eq { "issue_id" : issueID , "comment_id" : commentID } ) .
OrderBy ( "edited_unix DESC" ) .
Find ( & res )
if err != nil {
log . Error ( "can not fetch issue content history list. err=%v" , err )
return nil , err
}
for _ , item := range res {
2024-02-16 12:50:20 +03:00
if item . UserID > 0 {
item . UserAvatarLink = avatars . GenerateUserAvatarFastLink ( item . UserName , 0 )
} else {
item . UserAvatarLink = avatars . DefaultAvatarLink ( )
}
2021-10-11 01:40:03 +03:00
}
return res , nil
}
2021-11-22 15:20:16 +03:00
// HasIssueContentHistory check if a ContentHistory entry exists
2021-12-20 07:41:31 +03:00
func HasIssueContentHistory ( dbCtx context . Context , issueID , commentID int64 ) ( bool , error ) {
2024-03-01 19:46:02 +03:00
exists , err := db . GetEngine ( dbCtx ) . Where ( builder . Eq { "issue_id" : issueID , "comment_id" : commentID } ) . Exist ( & ContentHistory { } )
2021-11-22 15:20:16 +03:00
if err != nil {
2024-03-01 19:46:02 +03:00
return false , fmt . Errorf ( "can not check issue content history. err: %w" , err )
2021-11-22 15:20:16 +03:00
}
return exists , err
}
// SoftDeleteIssueContentHistory soft delete
2021-10-11 01:40:03 +03:00
func SoftDeleteIssueContentHistory ( dbCtx context . Context , historyID int64 ) error {
if _ , err := db . GetEngine ( dbCtx ) . ID ( historyID ) . Cols ( "is_deleted" , "content_text" ) . Update ( & ContentHistory {
IsDeleted : true ,
ContentText : "" ,
} ) ; err != nil {
log . Error ( "failed to soft delete issue content history. err=%v" , err )
return err
}
return nil
}
// ErrIssueContentHistoryNotExist not exist error
type ErrIssueContentHistoryNotExist struct {
ID int64
}
// Error error string
func ( err ErrIssueContentHistoryNotExist ) Error ( ) string {
return fmt . Sprintf ( "issue content history does not exist [id: %d]" , err . ID )
}
2022-10-18 08:50:37 +03:00
func ( err ErrIssueContentHistoryNotExist ) Unwrap ( ) error {
return util . ErrNotExist
}
2021-10-11 01:40:03 +03:00
// GetIssueContentHistoryByID get issue content history
func GetIssueContentHistoryByID ( dbCtx context . Context , id int64 ) ( * ContentHistory , error ) {
h := & ContentHistory { }
has , err := db . GetEngine ( dbCtx ) . ID ( id ) . Get ( h )
if err != nil {
return nil , err
} else if ! has {
return nil , ErrIssueContentHistoryNotExist { id }
}
return h , nil
}
// GetIssueContentHistoryAndPrev get a history and the previous non-deleted history (to compare)
2023-11-25 20:21:21 +03:00
func GetIssueContentHistoryAndPrev ( dbCtx context . Context , issueID , id int64 ) ( history , prevHistory * ContentHistory , err error ) {
2021-10-11 01:40:03 +03:00
history = & ContentHistory { }
2023-11-25 20:21:21 +03:00
has , err := db . GetEngine ( dbCtx ) . Where ( "id=? AND issue_id=?" , id , issueID ) . Get ( history )
2021-10-11 01:40:03 +03:00
if err != nil {
log . Error ( "failed to get issue content history %v. err=%v" , id , err )
return nil , nil , err
} else if ! has {
log . Error ( "issue content history does not exist. id=%v. err=%v" , id , err )
return nil , nil , & ErrIssueContentHistoryNotExist { id }
}
prevHistory = & ContentHistory { }
has , err = db . GetEngine ( dbCtx ) . Where ( builder . Eq { "issue_id" : history . IssueID , "comment_id" : history . CommentID , "is_deleted" : false } ) .
And ( builder . Lt { "edited_unix" : history . EditedUnix } ) .
OrderBy ( "edited_unix DESC" ) . Limit ( 1 ) .
Get ( prevHistory )
if err != nil {
log . Error ( "failed to get issue content history %v. err=%v" , id , err )
return nil , nil , err
} else if ! has {
return history , nil , nil
}
return history , prevHistory , nil
}