aboutsummaryrefslogtreecommitdiffstats
path: root/routers/web/repo/issue_content_history.go
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2021-10-11 06:40:03 +0800
committerGitHub <noreply@github.com>2021-10-10 18:40:03 -0400
commitc5c88f2f18d99a188357e0bcb837f94c9c41e79a (patch)
treeccdd155112a7ebedc93f0eb0310db860fd38b2d2 /routers/web/repo/issue_content_history.go
parentff9a8a22312a653702342ce0a4073ae8fde2b1d4 (diff)
downloadgitea-c5c88f2f18d99a188357e0bcb837f94c9c41e79a.tar.gz
gitea-c5c88f2f18d99a188357e0bcb837f94c9c41e79a.zip
Save and view issue/comment content history (#16909)
* issue content history * Use timeutil.TimeStampNow() for content history time instead of issue/comment.UpdatedUnix (which are not updated in time) * i18n for frontend * refactor * clean up * fix refactor * re-format * temp refactor * follow db refactor * rename IssueContentHistory to ContentHistory, remove empty model tags * fix html * use avatar refactor to generate avatar url * add unit test, keep at most 20 history revisions. * re-format * syntax nit * Add issue content history table * Update models/migrations/v197.go Co-authored-by: 6543 <6543@obermui.de> * fix merge Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Lauris BH <lauris@nix.lv>
Diffstat (limited to 'routers/web/repo/issue_content_history.go')
-rw-r--r--routers/web/repo/issue_content_history.go206
1 files changed, 206 insertions, 0 deletions
diff --git a/routers/web/repo/issue_content_history.go b/routers/web/repo/issue_content_history.go
new file mode 100644
index 0000000000..c0e958203d
--- /dev/null
+++ b/routers/web/repo/issue_content_history.go
@@ -0,0 +1,206 @@
+// 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 repo
+
+import (
+ "bytes"
+ "fmt"
+ "html"
+ "net/http"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/models/db"
+ issuesModel "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/timeutil"
+
+ "github.com/sergi/go-diff/diffmatchpatch"
+ "github.com/unknwon/i18n"
+)
+
+// GetContentHistoryOverview get overview
+func GetContentHistoryOverview(ctx *context.Context) {
+ issue := GetActionIssue(ctx)
+ if issue == nil {
+ return
+ }
+
+ lang := ctx.Data["Lang"].(string)
+ editedHistoryCountMap, _ := issuesModel.QueryIssueContentHistoryEditedCountMap(db.DefaultContext, issue.ID)
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "i18n": map[string]interface{}{
+ "textEdited": i18n.Tr(lang, "repo.issues.content_history.edited"),
+ "textDeleteFromHistory": i18n.Tr(lang, "repo.issues.content_history.delete_from_history"),
+ "textDeleteFromHistoryConfirm": i18n.Tr(lang, "repo.issues.content_history.delete_from_history_confirm"),
+ "textOptions": i18n.Tr(lang, "repo.issues.content_history.options"),
+ },
+ "editedHistoryCountMap": editedHistoryCountMap,
+ })
+}
+
+// GetContentHistoryList get list
+func GetContentHistoryList(ctx *context.Context) {
+ issue := GetActionIssue(ctx)
+ commentID := ctx.FormInt64("comment_id")
+ if issue == nil {
+ return
+ }
+
+ items, _ := issuesModel.FetchIssueContentHistoryList(db.DefaultContext, issue.ID, commentID)
+
+ // render history list to HTML for frontend dropdown items: (name, value)
+ // name is HTML of "avatar + userName + userAction + timeSince"
+ // value is historyId
+ lang := ctx.Data["Lang"].(string)
+ var results []map[string]interface{}
+ for _, item := range items {
+ var actionText string
+ if item.IsDeleted {
+ actionTextDeleted := i18n.Tr(lang, "repo.issues.content_history.deleted")
+ actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
+ } else if item.IsFirstCreated {
+ actionText = i18n.Tr(lang, "repo.issues.content_history.created")
+ } else {
+ actionText = i18n.Tr(lang, "repo.issues.content_history.edited")
+ }
+ timeSinceText := timeutil.TimeSinceUnix(item.EditedUnix, lang)
+ results = append(results, map[string]interface{}{
+ "name": fmt.Sprintf("<img class='ui avatar image' src='%s'><strong>%s</strong> %s %s",
+ html.EscapeString(item.UserAvatarLink), html.EscapeString(item.UserName), actionText, timeSinceText),
+ "value": item.HistoryID,
+ })
+ }
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "results": results,
+ })
+}
+
+// canSoftDeleteContentHistory checks whether current user can soft-delete a history revision
+// Admins or owners can always delete history revisions. Normal users can only delete own history revisions.
+func canSoftDeleteContentHistory(ctx *context.Context, issue *models.Issue, comment *models.Comment,
+ history *issuesModel.ContentHistory) bool {
+
+ canSoftDelete := false
+ if ctx.Repo.IsOwner() {
+ canSoftDelete = true
+ } else if ctx.Repo.CanWrite(models.UnitTypeIssues) {
+ canSoftDelete = ctx.User.ID == history.PosterID
+ if comment == nil {
+ canSoftDelete = canSoftDelete && (ctx.User.ID == issue.PosterID)
+ canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
+ } else {
+ canSoftDelete = canSoftDelete && (ctx.User.ID == comment.PosterID)
+ canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
+ canSoftDelete = canSoftDelete && (history.CommentID == comment.ID)
+ }
+ }
+ return canSoftDelete
+}
+
+//GetContentHistoryDetail get detail
+func GetContentHistoryDetail(ctx *context.Context) {
+ issue := GetActionIssue(ctx)
+ if issue == nil {
+ return
+ }
+
+ historyID := ctx.FormInt64("history_id")
+ history, prevHistory, err := issuesModel.GetIssueContentHistoryAndPrev(db.DefaultContext, historyID)
+ if err != nil {
+ ctx.JSON(http.StatusNotFound, map[string]interface{}{
+ "message": "Can not find the content history",
+ })
+ return
+ }
+
+ // get the related comment if this history revision is for a comment, otherwise the history revision is for an issue.
+ var comment *models.Comment
+ if history.CommentID != 0 {
+ var err error
+ if comment, err = models.GetCommentByID(history.CommentID); err != nil {
+ log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
+ return
+ }
+ }
+
+ // get the previous history revision (if exists)
+ var prevHistoryID int64
+ var prevHistoryContentText string
+ if prevHistory != nil {
+ prevHistoryID = prevHistory.ID
+ prevHistoryContentText = prevHistory.ContentText
+ }
+
+ // compare the current history revision with the previous one
+ dmp := diffmatchpatch.New()
+ diff := dmp.DiffMain(prevHistoryContentText, history.ContentText, true)
+ diff = dmp.DiffCleanupEfficiency(diff)
+
+ // use chroma to render the diff html
+ diffHTMLBuf := bytes.Buffer{}
+ diffHTMLBuf.WriteString("<pre class='chroma' style='tab-size: 4'>")
+ for _, it := range diff {
+ if it.Type == diffmatchpatch.DiffInsert {
+ diffHTMLBuf.WriteString("<span class='gi'>")
+ diffHTMLBuf.WriteString(html.EscapeString(it.Text))
+ diffHTMLBuf.WriteString("</span>")
+ } else if it.Type == diffmatchpatch.DiffDelete {
+ diffHTMLBuf.WriteString("<span class='gd'>")
+ diffHTMLBuf.WriteString(html.EscapeString(it.Text))
+ diffHTMLBuf.WriteString("</span>")
+ } else {
+ diffHTMLBuf.WriteString(html.EscapeString(it.Text))
+ }
+ }
+ diffHTMLBuf.WriteString("</pre>")
+
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "canSoftDelete": canSoftDeleteContentHistory(ctx, issue, comment, history),
+ "historyId": historyID,
+ "prevHistoryId": prevHistoryID,
+ "diffHtml": diffHTMLBuf.String(),
+ })
+}
+
+//SoftDeleteContentHistory soft delete
+func SoftDeleteContentHistory(ctx *context.Context) {
+ issue := GetActionIssue(ctx)
+ if issue == nil {
+ return
+ }
+
+ commentID := ctx.FormInt64("comment_id")
+ historyID := ctx.FormInt64("history_id")
+
+ var comment *models.Comment
+ var history *issuesModel.ContentHistory
+ var err error
+ if commentID != 0 {
+ if comment, err = models.GetCommentByID(commentID); err != nil {
+ log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
+ return
+ }
+ }
+ if history, err = issuesModel.GetIssueContentHistoryByID(db.DefaultContext, historyID); err != nil {
+ log.Error("can not get issue content history %v. err=%v", historyID, err)
+ return
+ }
+
+ canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
+ if !canSoftDelete {
+ ctx.JSON(http.StatusForbidden, map[string]interface{}{
+ "message": "Can not delete the content history",
+ })
+ return
+ }
+
+ err = issuesModel.SoftDeleteIssueContentHistory(db.DefaultContext, historyID)
+ log.Debug("soft delete issue content history. issue=%d, comment=%d, history=%d", issue.ID, commentID, historyID)
+ ctx.JSON(http.StatusOK, map[string]interface{}{
+ "ok": err == nil,
+ })
+}