mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-24 13:09:23 -05:00
58ee9fdc4a
Add `DiffCleanupSemantic` into the mix when generated diffs (PR review, commit view and issue/comment history). This avoids trying to produce a optimal diff and tries to reduce the amount of edits, by combing them into larger edits, which is nicer and easier to 'look at'. There's no need for a perfect minimal diff, as the output isn't being parsed by a computer, it's parsed by people. Ref: https://codeberg.org/forgejo/forgejo/issues/4996
237 lines
7.9 KiB
Go
237 lines
7.9 KiB
Go
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package repo
|
|
|
|
import (
|
|
"bytes"
|
|
"html"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"code.gitea.io/gitea/models/avatars"
|
|
issues_model "code.gitea.io/gitea/models/issues"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/templates"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
"code.gitea.io/gitea/services/context"
|
|
|
|
"github.com/sergi/go-diff/diffmatchpatch"
|
|
)
|
|
|
|
// GetContentHistoryOverview get overview
|
|
func GetContentHistoryOverview(ctx *context.Context) {
|
|
issue := GetActionIssue(ctx)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
editedHistoryCountMap, _ := issues_model.QueryIssueContentHistoryEditedCountMap(ctx, issue.ID)
|
|
ctx.JSON(http.StatusOK, map[string]any{
|
|
"i18n": map[string]any{
|
|
"textEdited": ctx.Tr("repo.issues.content_history.edited"),
|
|
"textDeleteFromHistory": ctx.Tr("repo.issues.content_history.delete_from_history"),
|
|
"textDeleteFromHistoryConfirm": ctx.Tr("repo.issues.content_history.delete_from_history_confirm"),
|
|
"textOptions": ctx.Tr("repo.issues.content_history.options"),
|
|
},
|
|
"editedHistoryCountMap": editedHistoryCountMap,
|
|
})
|
|
}
|
|
|
|
// GetContentHistoryList get list
|
|
func GetContentHistoryList(ctx *context.Context) {
|
|
issue := GetActionIssue(ctx)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
commentID := ctx.FormInt64("comment_id")
|
|
items, _ := issues_model.FetchIssueContentHistoryList(ctx, 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
|
|
var results []map[string]any
|
|
for _, item := range items {
|
|
var actionText string
|
|
if item.IsDeleted {
|
|
actionTextDeleted := ctx.Locale.TrString("repo.issues.content_history.deleted")
|
|
actionText = "<i data-history-is-deleted='1'>" + actionTextDeleted + "</i>"
|
|
} else if item.IsFirstCreated {
|
|
actionText = ctx.Locale.TrString("repo.issues.content_history.created")
|
|
} else {
|
|
actionText = ctx.Locale.TrString("repo.issues.content_history.edited")
|
|
}
|
|
|
|
username := item.UserName
|
|
if setting.UI.DefaultShowFullName && strings.TrimSpace(item.UserFullName) != "" {
|
|
username = strings.TrimSpace(item.UserFullName)
|
|
}
|
|
|
|
src := html.EscapeString(item.UserAvatarLink)
|
|
class := avatars.DefaultAvatarClass + " tw-mr-2"
|
|
name := html.EscapeString(username)
|
|
avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
|
|
timeSinceText := string(timeutil.TimeSinceUnix(item.EditedUnix, ctx.Locale))
|
|
|
|
results = append(results, map[string]any{
|
|
"name": avatarHTML + "<strong>" + name + "</strong> " + actionText + " " + timeSinceText,
|
|
"value": item.HistoryID,
|
|
})
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, map[string]any{
|
|
"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 *issues_model.Issue, comment *issues_model.Comment,
|
|
history *issues_model.ContentHistory,
|
|
) (canSoftDelete bool) {
|
|
// CanWrite means the doer can manage the issue/PR list
|
|
if ctx.Repo.IsOwner() || ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
|
|
canSoftDelete = true
|
|
} else if ctx.Doer == nil {
|
|
canSoftDelete = false
|
|
} else {
|
|
// for read-only users, they could still post issues or comments,
|
|
// they should be able to delete the history related to their own issue/comment, a case is:
|
|
// 1. the user posts some sensitive data
|
|
// 2. then the repo owner edits the post but didn't remove the sensitive data
|
|
// 3. the poster wants to delete the edited history revision
|
|
if comment == nil {
|
|
// the issue poster or the history poster can soft-delete
|
|
canSoftDelete = ctx.Doer.ID == issue.PosterID || ctx.Doer.ID == history.PosterID
|
|
canSoftDelete = canSoftDelete && (history.IssueID == issue.ID)
|
|
} else {
|
|
// the comment poster or the history poster can soft-delete
|
|
canSoftDelete = ctx.Doer.ID == comment.PosterID || ctx.Doer.ID == history.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 ctx.Written() {
|
|
return
|
|
}
|
|
|
|
historyID := ctx.FormInt64("history_id")
|
|
history, prevHistory, err := issues_model.GetIssueContentHistoryAndPrev(ctx, issue.ID, historyID)
|
|
if err != nil {
|
|
ctx.JSON(http.StatusNotFound, map[string]any{
|
|
"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 *issues_model.Comment
|
|
if history.CommentID != 0 {
|
|
var err error
|
|
if comment, err = issues_model.GetCommentByID(ctx, 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()
|
|
// `checklines=false` makes better diff result
|
|
diff := dmp.DiffMain(prevHistoryContentText, history.ContentText, false)
|
|
diff = dmp.DiffCleanupSemantic(diff)
|
|
diff = dmp.DiffCleanupEfficiency(diff)
|
|
|
|
// use chroma to render the diff html
|
|
diffHTMLBuf := bytes.Buffer{}
|
|
diffHTMLBuf.WriteString("<pre class='chroma'>")
|
|
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]any{
|
|
"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 ctx.Written() {
|
|
return
|
|
}
|
|
|
|
commentID := ctx.FormInt64("comment_id")
|
|
historyID := ctx.FormInt64("history_id")
|
|
|
|
var comment *issues_model.Comment
|
|
var history *issues_model.ContentHistory
|
|
var err error
|
|
|
|
if history, err = issues_model.GetIssueContentHistoryByID(ctx, historyID); err != nil {
|
|
log.Error("can not get issue content history %v. err=%v", historyID, err)
|
|
return
|
|
}
|
|
if history.IssueID != issue.ID {
|
|
ctx.NotFound("CompareRepoID", issues_model.ErrCommentNotExist{})
|
|
return
|
|
}
|
|
if commentID != 0 {
|
|
if history.CommentID != commentID {
|
|
ctx.NotFound("CompareCommentID", issues_model.ErrCommentNotExist{})
|
|
return
|
|
}
|
|
|
|
if comment, err = issues_model.GetCommentByID(ctx, commentID); err != nil {
|
|
log.Error("can not get comment for issue content history %v. err=%v", historyID, err)
|
|
return
|
|
}
|
|
if comment.IssueID != issue.ID {
|
|
ctx.NotFound("CompareIssueID", issues_model.ErrCommentNotExist{})
|
|
return
|
|
}
|
|
}
|
|
|
|
canSoftDelete := canSoftDeleteContentHistory(ctx, issue, comment, history)
|
|
if !canSoftDelete {
|
|
ctx.JSON(http.StatusForbidden, map[string]any{
|
|
"message": "Can not delete the content history",
|
|
})
|
|
return
|
|
}
|
|
|
|
err = issues_model.SoftDeleteIssueContentHistory(ctx, historyID)
|
|
log.Debug("soft delete issue content history. issue=%d, comment=%d, history=%d", issue.ID, commentID, historyID)
|
|
ctx.JSON(http.StatusOK, map[string]any{
|
|
"ok": err == nil,
|
|
})
|
|
}
|