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  }