diff options
author | wxiaoguang <wxiaoguang@gmail.com> | 2021-10-11 06:40:03 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-10 18:40:03 -0400 |
commit | c5c88f2f18d99a188357e0bcb837f94c9c41e79a (patch) | |
tree | ccdd155112a7ebedc93f0eb0310db860fd38b2d2 /routers/web | |
parent | ff9a8a22312a653702342ce0a4073ae8fde2b1d4 (diff) | |
download | gitea-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')
-rw-r--r-- | routers/web/repo/issue_content_history.go | 206 | ||||
-rw-r--r-- | routers/web/web.go | 8 |
2 files changed, 214 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, + }) +} diff --git a/routers/web/web.go b/routers/web/web.go index b4103ccad3..caec067671 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -732,6 +732,9 @@ func RegisterRoutes(m *web.Route) { m.Get("/attachments", repo.GetIssueAttachments) m.Get("/attachments/{uuid}", repo.GetAttachment) }) + m.Group("/{index}", func() { + m.Post("/content-history/soft-delete", repo.SoftDeleteContentHistory) + }) m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel) m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone) @@ -853,6 +856,11 @@ func RegisterRoutes(m *web.Route) { m.Group("", func() { m.Get("/{type:issues|pulls}", repo.Issues) m.Get("/{type:issues|pulls}/{index}", repo.ViewIssue) + m.Group("/{type:issues|pulls}/{index}/content-history", func() { + m.Get("/overview", repo.GetContentHistoryOverview) + m.Get("/list", repo.GetContentHistoryList) + m.Get("/detail", repo.GetContentHistoryDetail) + }) m.Get("/labels", reqRepoIssuesOrPullsReader, repo.RetrieveLabels, repo.Labels) m.Get("/milestones", reqRepoIssuesOrPullsReader, repo.Milestones) }, context.RepoRef()) |