Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Save and view issue/comment content history #16909

Merged
merged 33 commits into from
Oct 10, 2021
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
5c10ada
issue content history
wxiaoguang Aug 31, 2021
c873f1b
Merge branch 'main' into issue-content-history
wxiaoguang Sep 1, 2021
e3a706f
Use timeutil.TimeStampNow() for content history time instead of issue…
wxiaoguang Sep 1, 2021
c33a9f7
i18n for frontend
wxiaoguang Sep 4, 2021
337293e
Merge branch 'main' into issue-content-history
wxiaoguang Sep 4, 2021
58224f4
refactor
wxiaoguang Sep 5, 2021
f9ab9f5
Merge branch 'issue-content-history' of github.com:wxiaoguang/gitea i…
wxiaoguang Sep 5, 2021
7952ccf
clean up
wxiaoguang Sep 7, 2021
177e3ac
Merge remote-tracking branch 'go-gitea/main' into issue-content-history
wxiaoguang Sep 7, 2021
fd792fd
Merge branch 'main' into issue-content-history
zeripath Sep 14, 2021
d52c9cd
Merge branch 'main' into issue-content-history
wxiaoguang Sep 18, 2021
2afea36
Merge branch 'main' into issue-content-history
wxiaoguang Sep 22, 2021
7f71a28
fix refactor
wxiaoguang Sep 22, 2021
88e81f0
re-format
wxiaoguang Sep 22, 2021
25573a9
temp refactor
wxiaoguang Sep 22, 2021
c7fe79d
Merge branch 'main' into issue-content-history
wxiaoguang Sep 26, 2021
b21a41b
follow db refactor
wxiaoguang Sep 26, 2021
8416fc8
rename IssueContentHistory to ContentHistory, remove empty model tags
wxiaoguang Sep 26, 2021
d8a63af
fix html
wxiaoguang Sep 26, 2021
bdbec82
Merge branch 'main' into issue-content-history
wxiaoguang Sep 27, 2021
7160036
Merge branch 'main' into issue-content-history
wxiaoguang Oct 6, 2021
54ad038
use avatar refactor to generate avatar url
wxiaoguang Oct 6, 2021
9f96c7b
add unit test, keep at most 20 history revisions.
wxiaoguang Oct 8, 2021
3f23204
Merge branch 'main' into issue-content-history
wxiaoguang Oct 8, 2021
a70920d
re-format
wxiaoguang Oct 8, 2021
78cd825
syntax nit
6543 Oct 8, 2021
85d8493
Merge branch 'main' into issue-content-history
lafriks Oct 8, 2021
5e7daaa
Add issue content history table
wxiaoguang Oct 8, 2021
f613828
Merge remote-tracking branch 'go-gitea/main' into issue-content-history
wxiaoguang Oct 8, 2021
f37c287
Update models/migrations/v197.go
wxiaoguang Oct 8, 2021
357a5e5
Merge branch 'main' into issue-content-history
wxiaoguang Oct 8, 2021
a5f551e
fix merge
wxiaoguang Oct 8, 2021
578cc71
Merge branch 'main' into issue-content-history
lafriks Oct 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions models/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -815,6 +815,8 @@ func (issue *Issue) ChangeContent(doer *User, content string) (err error) {
return fmt.Errorf("UpdateIssueCols: %v", err)
}

SaveIssueContentHistory(ctx.Engine(), issue.PosterID, issue.ID, 0, timeutil.TimeStampNow(), issue.Content, false)

if err = issue.addCrossReferences(ctx.Engine(), doer, true); err != nil {
return err
}
Expand Down Expand Up @@ -984,6 +986,9 @@ func newIssue(e db.Engine, doer *User, opts NewIssueOptions) (err error) {
if err = opts.Issue.loadAttributes(e); err != nil {
return err
}

SaveIssueContentHistory(e, opts.Issue.PosterID, opts.Issue.ID, 0, timeutil.TimeStampNow(), opts.Issue.Content, true)

return opts.Issue.addCrossReferences(e, doer, false)
}

Expand Down Expand Up @@ -2144,6 +2149,12 @@ func UpdateReactionsMigrationsByType(gitServiceType structs.GitServiceType, orig
func deleteIssuesByRepoID(sess db.Engine, repoID int64) (attachmentPaths []string, err error) {
deleteCond := builder.Select("id").From("issue").Where(builder.Eq{"issue.repo_id": repoID})

// Delete content histories
if _, err = sess.In("issue_id", deleteCond).
Delete(&IssueContentHistory{}); err != nil {
return
}

// Delete comments and attachments
if _, err = sess.In("issue_id", deleteCond).
Delete(&Comment{}); err != nil {
Expand Down
6 changes: 6 additions & 0 deletions models/issue_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,12 @@ func deleteComment(e db.Engine, comment *Comment) error {
return err
}

if _, err := e.Delete(&IssueContentHistory{
CommentID: comment.ID,
}); err != nil {
return err
}

if comment.Type == CommentTypeComment {
if _, err := e.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
return err
Expand Down
177 changes: 177 additions & 0 deletions models/issue_content_history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package models

import (
"fmt"

"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/timeutil"

"xorm.io/builder"
)

// IssueContentHistory save issue/comment content history revisions.
type IssueContentHistory struct {
ID int64 `xorm:"pk autoincr"`
PosterID int64 `xorm:""`
IssueID int64 `xorm:"INDEX"`
CommentID int64 `xorm:"INDEX"`
EditedUnix timeutil.TimeStamp `xorm:"INDEX"`
ContentText string `xorm:"LONGTEXT"`
IsFirstCreated bool `xorm:""`
IsDeleted bool `xorm:""`
}

func init() {
db.RegisterModel(new(IssueContentHistory))
}

// SaveIssueContentHistory save history
func SaveIssueContentHistory(e db.Engine, posterID, issueID, commentID int64, editTime timeutil.TimeStamp, contentText string, isFirstCreated bool) {
ch := &IssueContentHistory{
PosterID: posterID,
IssueID: issueID,
CommentID: commentID,
ContentText: contentText,
EditedUnix: editTime,
IsFirstCreated: isFirstCreated,
}
_, err := e.Insert(ch)
if err != nil {
log.Error("can not save issue content history. err=%v", err)
}
}

// 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(e db.Engine, issueID int64) map[int64]int {
type HistoryCountRecord struct {
CommentID int64
HistoryCount int
}
records := make([]*HistoryCountRecord, 0)

err := e.Select("comment_id, COUNT(1) as history_count").
Table("issue_content_history").
Where(builder.Eq{"issue_id": issueID}).
GroupBy("comment_id").
Having("history_count > 1").
Find(&records)
if err != nil {
log.Error("can not query issue content history count map. err=%v", err)
}

res := map[int64]int{}
for _, r := range records {
res[r.CommentID] = r.HistoryCount
}
return res
}

// IssueContentListItem the list for web ui
type IssueContentListItem struct {
UserID int64
UserName string
UserAvatar string
UserAvatarEmail string
UseCustomAvatar bool
UserAvatarLink string

HistoryID int64
EditedUnix timeutil.TimeStamp
IsFirstCreated bool
IsDeleted bool
}

// FetchIssueContentHistoryList fetch list
func FetchIssueContentHistoryList(e db.Engine, issueID int64, commentID int64) []*IssueContentListItem {
res := make([]*IssueContentListItem, 0)
err := e.Select("u.id as user_id, u.name as user_name,"+
"u.avatar as user_avatar, u.avatar_email as user_avatar_email, u.use_custom_avatar,"+
"h.id as history_id, h.edited_unix, h.is_first_created, h.is_deleted").
Table([]string{"issue_content_history", "h"}).
Join("INNER", []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)
}

for _, item := range res {
u := &User{
ID: item.UserID,
Name: item.UserName,
Avatar: item.UserAvatar,
AvatarEmail: item.UserAvatarEmail,
UseCustomAvatar: item.UseCustomAvatar,
}
item.UserAvatarLink = u.AvatarLink()
}
return res
}

//SoftDeleteIssueContentHistory soft delete
func SoftDeleteIssueContentHistory(e db.Engine, historyID int64) {
if _, err := e.ID(historyID).Cols("is_deleted", "content_text").Update(&IssueContentHistory{
IsDeleted: true,
ContentText: "",
}); err != nil {
log.Error("failed to soft delete issue content history. err=%v", err)
}
}

// 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)
}

// GetIssueContentHistoryByID get issue content history
func GetIssueContentHistoryByID(e db.Engine, id int64) (*IssueContentHistory, error) {
h := &IssueContentHistory{}
has, err := e.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)
func GetIssueContentHistoryAndPrev(e db.Engine, id int64) (history, prevHistory *IssueContentHistory) {
history = &IssueContentHistory{}
has, err := e.ID(id).Get(history)
if err != nil {
log.Error("failed to get issue content history %v. err=%v", id, err)
return nil, nil
} else if !has {
log.Error("issue content history does not exist. id=%v. err=%v", id, err)
return nil, nil
}

prevHistory = &IssueContentHistory{}
has, err = e.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
} else if !has {
return history, nil
}

return history, prevHistory
}
6 changes: 6 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1376,6 +1376,12 @@ issues.review.un_resolve_conversation = Unresolve conversation
issues.review.resolved_by = marked this conversation as resolved
issues.assignee.error = Not all assignees was added due to an unexpected error.
issues.reference_issue.body = Body
issues.content_history.deleted = deleted
issues.content_history.edited = edited
issues.content_history.created = created
issues.content_history.delete_from_history = Delete from history
issues.content_history.delete_from_history_confirm = Delete from history?
issues.content_history.options = Options

compare.compare_base = base
compare.compare_head = compare
Expand Down
Loading