feat: vendor gitea 1.16.2
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"html/template"
|
||||
"io"
|
||||
"net/url"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -23,18 +24,23 @@ import (
|
||||
pull_model "code.gitea.io/gitea/models/pull"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/analyze"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/charset"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/git/attribute"
|
||||
"code.gitea.io/gitea/modules/git/gitcmd"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/highlight"
|
||||
"code.gitea.io/gitea/modules/htmlutil"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/svg"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/alecthomas/chroma/v2"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
stdcharset "golang.org/x/net/html/charset"
|
||||
"golang.org/x/text/encoding"
|
||||
@@ -64,18 +70,6 @@ const (
|
||||
DiffFileCopy
|
||||
)
|
||||
|
||||
// DiffLineExpandDirection represents the DiffLineSection expand direction
|
||||
type DiffLineExpandDirection uint8
|
||||
|
||||
// DiffLineExpandDirection possible values.
|
||||
const (
|
||||
DiffLineExpandNone DiffLineExpandDirection = iota + 1
|
||||
DiffLineExpandSingle
|
||||
DiffLineExpandUpDown
|
||||
DiffLineExpandUp
|
||||
DiffLineExpandDown
|
||||
)
|
||||
|
||||
// DiffLine represents a line difference in a DiffSection.
|
||||
type DiffLine struct {
|
||||
LeftIdx int // line number, 1-based
|
||||
@@ -89,13 +83,37 @@ type DiffLine struct {
|
||||
|
||||
// DiffLineSectionInfo represents diff line section meta data
|
||||
type DiffLineSectionInfo struct {
|
||||
Path string
|
||||
LastLeftIdx int
|
||||
LastRightIdx int
|
||||
LeftIdx int
|
||||
RightIdx int
|
||||
language *diffVarMutable[string]
|
||||
|
||||
Path string
|
||||
|
||||
// These line "idx" are 1-based line numbers
|
||||
// Left/Right refer to the left/right side of the diff:
|
||||
//
|
||||
// LastLeftIdx | LastRightIdx
|
||||
// [up/down expander] @@ hunk info @@
|
||||
// LeftIdx | RightIdx
|
||||
|
||||
LastLeftIdx int
|
||||
LastRightIdx int
|
||||
LeftIdx int
|
||||
RightIdx int
|
||||
|
||||
// Hunk sizes of the hidden lines
|
||||
LeftHunkSize int
|
||||
RightHunkSize int
|
||||
|
||||
// For example:
|
||||
// 17 | 31
|
||||
// [up/down] @@ -40,23 +54,9 @@ ....
|
||||
// 40 | 54
|
||||
//
|
||||
// In this case:
|
||||
// LastLeftIdx = 17, LastRightIdx = 31
|
||||
// LeftHunkSize = 23, RightHunkSize = 9
|
||||
// LeftIdx = 40, RightIdx = 54
|
||||
|
||||
HiddenCommentIDs []int64 // IDs of hidden comments in this section
|
||||
}
|
||||
|
||||
// DiffHTMLOperation is the HTML version of diffmatchpatch.Diff
|
||||
@@ -107,8 +125,14 @@ type DiffHTMLOperation struct {
|
||||
// BlobExcerptChunkSize represent max lines of excerpt
|
||||
const BlobExcerptChunkSize = 20
|
||||
|
||||
// MaxDiffHighlightEntireFileSize is the maximum file size that will be highlighted with "entire file diff"
|
||||
const MaxDiffHighlightEntireFileSize = 1 * 1024 * 1024
|
||||
// Chroma seems extremely slow when highlighting large files, it might take dozens or hundreds of milliseconds.
|
||||
// When fully highlighting a diff with a lot of large files, it would take many seconds or even dozens of seconds.
|
||||
// So, don't highlight the entire file if it's too large, or highlighting takes too long.
|
||||
// When there is no full-file highlighting, the legacy "line-by-line" highlighting is still applied as the fallback.
|
||||
const (
|
||||
MaxFullFileHighlightSizeLimit = 256 * 1024
|
||||
MaxFullFileHighlightTimeLimit = 2 * time.Second
|
||||
)
|
||||
|
||||
// GetType returns the type of DiffLine.
|
||||
func (d *DiffLine) GetType() int {
|
||||
@@ -150,40 +174,117 @@ func (d *DiffLine) GetLineTypeMarker() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetBlobExcerptQuery builds query string to get blob excerpt
|
||||
func (d *DiffLine) GetBlobExcerptQuery() string {
|
||||
query := fmt.Sprintf(
|
||||
func (d *DiffLine) getBlobExcerptQuery() string {
|
||||
language := ""
|
||||
if d.SectionInfo.language != nil { // for normal cases, it can't be nil, this check is only for some tests
|
||||
language = d.SectionInfo.language.value
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"last_left=%d&last_right=%d&"+
|
||||
"left=%d&right=%d&"+
|
||||
"left_hunk_size=%d&right_hunk_size=%d&"+
|
||||
"path=%s",
|
||||
"path=%s&filelang=%s",
|
||||
d.SectionInfo.LastLeftIdx, d.SectionInfo.LastRightIdx,
|
||||
d.SectionInfo.LeftIdx, d.SectionInfo.RightIdx,
|
||||
d.SectionInfo.LeftHunkSize, d.SectionInfo.RightHunkSize,
|
||||
url.QueryEscape(d.SectionInfo.Path))
|
||||
return query
|
||||
url.QueryEscape(d.SectionInfo.Path), url.QueryEscape(language))
|
||||
}
|
||||
|
||||
// GetExpandDirection gets DiffLineExpandDirection
|
||||
func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection {
|
||||
func (d *DiffLine) GetExpandDirection() string {
|
||||
if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.LeftIdx-d.SectionInfo.LastLeftIdx <= 1 || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 {
|
||||
return DiffLineExpandNone
|
||||
return ""
|
||||
}
|
||||
if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 {
|
||||
return DiffLineExpandUp
|
||||
} else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 {
|
||||
return DiffLineExpandUpDown
|
||||
return "up"
|
||||
} else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx-1 > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 {
|
||||
return "updown"
|
||||
} else if d.SectionInfo.LeftHunkSize <= 0 && d.SectionInfo.RightHunkSize <= 0 {
|
||||
return DiffLineExpandDown
|
||||
return "down"
|
||||
}
|
||||
return DiffLineExpandSingle
|
||||
return "single"
|
||||
}
|
||||
|
||||
func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {
|
||||
type DiffBlobExcerptData struct {
|
||||
BaseLink string
|
||||
IsWikiRepo bool
|
||||
PullIssueIndex int64
|
||||
DiffStyle string
|
||||
AfterCommitID string
|
||||
}
|
||||
|
||||
const (
|
||||
DiffStyleSplit = "split"
|
||||
DiffStyleUnified = "unified"
|
||||
)
|
||||
|
||||
func (d *DiffLine) RenderBlobExcerptButtons(fileNameHash string, data *DiffBlobExcerptData) template.HTML {
|
||||
dataHiddenCommentIDs := strings.Join(base.Int64sToStrings(d.SectionInfo.HiddenCommentIDs), ",")
|
||||
anchor := fmt.Sprintf("diff-%sK%d", fileNameHash, d.SectionInfo.RightIdx)
|
||||
|
||||
makeButton := func(direction, svgName string) template.HTML {
|
||||
style := util.IfZero(data.DiffStyle, "unified")
|
||||
link := data.BaseLink + "/" + data.AfterCommitID + fmt.Sprintf("?style=%s&direction=%s&anchor=%s", url.QueryEscape(style), direction, url.QueryEscape(anchor)) + "&" + d.getBlobExcerptQuery()
|
||||
if data.PullIssueIndex > 0 {
|
||||
link += fmt.Sprintf("&pull_issue_index=%d", data.PullIssueIndex)
|
||||
}
|
||||
return htmlutil.HTMLFormat(
|
||||
`<button class="code-expander-button" hx-target="closest tr" hx-get="%s" data-hidden-comment-ids=",%s,">%s</button>`,
|
||||
link, dataHiddenCommentIDs, svg.RenderHTML(svgName),
|
||||
)
|
||||
}
|
||||
var content template.HTML
|
||||
|
||||
if len(d.SectionInfo.HiddenCommentIDs) > 0 {
|
||||
tooltip := fmt.Sprintf("%d hidden comment(s)", len(d.SectionInfo.HiddenCommentIDs))
|
||||
content += htmlutil.HTMLFormat(`<span class="code-comment-more" data-tooltip-content="%s">%d</span>`, tooltip, len(d.SectionInfo.HiddenCommentIDs))
|
||||
}
|
||||
|
||||
expandDirection := d.GetExpandDirection()
|
||||
if expandDirection == "updown" || expandDirection == "down" {
|
||||
content += makeButton("down", "octicon-fold-down")
|
||||
}
|
||||
if expandDirection == "up" || expandDirection == "updown" {
|
||||
content += makeButton("up", "octicon-fold-up")
|
||||
}
|
||||
if expandDirection == "single" {
|
||||
content += makeButton("single", "octicon-fold")
|
||||
}
|
||||
return htmlutil.HTMLFormat(`<div class="code-expander-buttons" data-expand-direction="%s">%s</div>`, expandDirection, content)
|
||||
}
|
||||
|
||||
// FillHiddenCommentIDsForDiffLine finds comment IDs that are in the hidden range of an expand button
|
||||
func FillHiddenCommentIDsForDiffLine(line *DiffLine, lineComments map[int64][]*issues_model.Comment) {
|
||||
if line.Type != DiffLineSection {
|
||||
return
|
||||
}
|
||||
|
||||
var hiddenCommentIDs []int64
|
||||
for commentLineNum, comments := range lineComments {
|
||||
if commentLineNum < 0 {
|
||||
// ATTENTION: BLOB-EXCERPT-COMMENT-RIGHT: skip left-side, unchanged lines always use "right (proposed)" side for comments
|
||||
continue
|
||||
}
|
||||
lineNum := int(commentLineNum)
|
||||
isEndOfFileExpansion := line.SectionInfo.RightHunkSize == 0
|
||||
inRange := lineNum > line.SectionInfo.LastRightIdx &&
|
||||
(isEndOfFileExpansion && lineNum <= line.SectionInfo.RightIdx ||
|
||||
!isEndOfFileExpansion && lineNum < line.SectionInfo.RightIdx)
|
||||
|
||||
if inRange {
|
||||
for _, comment := range comments {
|
||||
hiddenCommentIDs = append(hiddenCommentIDs, comment.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
line.SectionInfo.HiddenCommentIDs = hiddenCommentIDs
|
||||
}
|
||||
|
||||
func newDiffLineSectionInfo(curFile *DiffFile, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {
|
||||
leftLine, leftHunk, rightLine, rightHunk := git.ParseDiffHunkString(line)
|
||||
|
||||
return &DiffLineSectionInfo{
|
||||
Path: treePath,
|
||||
Path: curFile.Name,
|
||||
language: &curFile.language,
|
||||
LastLeftIdx: lastLeftIdx,
|
||||
LastRightIdx: lastRightIdx,
|
||||
LeftIdx: leftLine,
|
||||
@@ -203,7 +304,11 @@ func getLineContent(content string, locale translation.Locale) DiffInline {
|
||||
|
||||
// DiffSection represents a section of a DiffFile.
|
||||
type DiffSection struct {
|
||||
file *DiffFile
|
||||
language *diffVarMutable[string]
|
||||
highlightedLeftLines *diffVarMutable[map[int]template.HTML]
|
||||
highlightedRightLines *diffVarMutable[map[int]template.HTML]
|
||||
highlightLexer *diffVarMutable[chroma.Lexer]
|
||||
|
||||
FileName string
|
||||
Lines []*DiffLine
|
||||
}
|
||||
@@ -244,17 +349,19 @@ func (diffSection *DiffSection) getLineContentForRender(lineIdx int, diffLine *D
|
||||
if setting.Git.DisableDiffHighlight {
|
||||
return template.HTML(html.EscapeString(diffLine.Content[1:]))
|
||||
}
|
||||
h, _ = highlight.Code(diffSection.FileName, fileLanguage, diffLine.Content[1:])
|
||||
return h
|
||||
if diffSection.highlightLexer.value == nil {
|
||||
diffSection.highlightLexer.value = highlight.DetectChromaLexerByFileName(diffSection.FileName, fileLanguage)
|
||||
}
|
||||
return highlight.RenderCodeByLexer(diffSection.highlightLexer.value, diffLine.Content[1:])
|
||||
}
|
||||
|
||||
func (diffSection *DiffSection) getDiffLineForRender(diffLineType DiffLineType, leftLine, rightLine *DiffLine, locale translation.Locale) DiffInline {
|
||||
var fileLanguage string
|
||||
var highlightedLeftLines, highlightedRightLines map[int]template.HTML
|
||||
// when a "diff section" is manually prepared by ExcerptBlob, it doesn't have "file" information
|
||||
if diffSection.file != nil {
|
||||
fileLanguage = diffSection.file.Language
|
||||
highlightedLeftLines, highlightedRightLines = diffSection.file.highlightedLeftLines, diffSection.file.highlightedRightLines
|
||||
if diffSection.language != nil {
|
||||
fileLanguage = diffSection.language.value
|
||||
highlightedLeftLines, highlightedRightLines = diffSection.highlightedLeftLines.value, diffSection.highlightedRightLines.value
|
||||
}
|
||||
|
||||
var lineHTML template.HTML
|
||||
@@ -288,6 +395,12 @@ func (diffSection *DiffSection) getDiffLineForRender(diffLineType DiffLineType,
|
||||
|
||||
// GetComputedInlineDiffFor computes inline diff for the given line.
|
||||
func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, locale translation.Locale) DiffInline {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// the logic is too complex in this function, help to catch any panic because Golang template doesn't print the stack
|
||||
log.Error("panic in GetComputedInlineDiffFor: %v\nStack: %s", err, log.Stack(2))
|
||||
}
|
||||
}()
|
||||
// try to find equivalent diff line. ignore, otherwise
|
||||
switch diffLine.Type {
|
||||
case DiffLineSection:
|
||||
@@ -305,33 +418,37 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, loc
|
||||
}
|
||||
}
|
||||
|
||||
// diffVarMutable is a wrapper to make a variable mutable to be shared across structs
|
||||
type diffVarMutable[T any] struct {
|
||||
value T
|
||||
}
|
||||
|
||||
// DiffFile represents a file diff.
|
||||
type DiffFile struct {
|
||||
// only used internally to parse Ambiguous filenames
|
||||
isAmbiguous bool
|
||||
|
||||
// basic fields (parsed from diff result)
|
||||
Name string
|
||||
NameHash string
|
||||
OldName string
|
||||
Addition int
|
||||
Deletion int
|
||||
Type DiffFileType
|
||||
Mode string
|
||||
OldMode string
|
||||
IsCreated bool
|
||||
IsDeleted bool
|
||||
IsBin bool
|
||||
IsLFSFile bool
|
||||
IsRenamed bool
|
||||
IsSubmodule bool
|
||||
Name string
|
||||
NameHash string
|
||||
OldName string
|
||||
Addition int
|
||||
Deletion int
|
||||
Type DiffFileType
|
||||
EntryMode string
|
||||
OldEntryMode string
|
||||
IsCreated bool
|
||||
IsDeleted bool
|
||||
IsBin bool
|
||||
IsLFSFile bool
|
||||
IsRenamed bool
|
||||
IsSubmodule bool
|
||||
// basic fields but for render purpose only
|
||||
Sections []*DiffSection
|
||||
IsIncomplete bool
|
||||
IsIncompleteLineTooLong bool
|
||||
|
||||
// will be filled by the extra loop in GitDiffForRender
|
||||
Language string
|
||||
IsGenerated bool
|
||||
IsVendored bool
|
||||
SubmoduleDiffInfo *SubmoduleDiffInfo // IsSubmodule==true, then there must be a SubmoduleDiffInfo
|
||||
@@ -343,9 +460,11 @@ type DiffFile struct {
|
||||
IsViewed bool // User specific
|
||||
HasChangedSinceLastReview bool // User specific
|
||||
|
||||
// for render purpose only, will be filled by the extra loop in GitDiffForRender
|
||||
highlightedLeftLines map[int]template.HTML
|
||||
highlightedRightLines map[int]template.HTML
|
||||
// for render purpose only, will be filled by the extra loop in GitDiffForRender, the maps of lines are 0-based
|
||||
language diffVarMutable[string]
|
||||
highlightRender diffVarMutable[chroma.Lexer] // cache render (atm: lexer) for current file, only detect once for line-by-line mode
|
||||
highlightedLeftLines diffVarMutable[map[int]template.HTML]
|
||||
highlightedRightLines diffVarMutable[map[int]template.HTML]
|
||||
}
|
||||
|
||||
// GetType returns type of diff file.
|
||||
@@ -382,6 +501,7 @@ func (diffFile *DiffFile) GetTailSectionAndLimitedContent(leftCommit, rightCommi
|
||||
Type: DiffLineSection,
|
||||
Content: " ",
|
||||
SectionInfo: &DiffLineSectionInfo{
|
||||
language: &diffFile.language,
|
||||
Path: diffFile.Name,
|
||||
LastLeftIdx: lastLine.LeftIdx,
|
||||
LastRightIdx: lastLine.RightIdx,
|
||||
@@ -401,25 +521,48 @@ func (diffFile *DiffFile) GetDiffFileName() string {
|
||||
return diffFile.Name
|
||||
}
|
||||
|
||||
// GetDiffFileBaseName returns the short name of the diff file, or its short old name in case it was deleted
|
||||
func (diffFile *DiffFile) GetDiffFileBaseName() string {
|
||||
if diffFile.Name == "" {
|
||||
return path.Base(diffFile.OldName)
|
||||
}
|
||||
return path.Base(diffFile.Name)
|
||||
}
|
||||
|
||||
func (diffFile *DiffFile) ShouldBeHidden() bool {
|
||||
return diffFile.IsGenerated || diffFile.IsViewed
|
||||
}
|
||||
|
||||
func (diffFile *DiffFile) ModeTranslationKey(mode string) string {
|
||||
switch mode {
|
||||
case "040000":
|
||||
return "git.filemode.directory"
|
||||
case "100644":
|
||||
return "git.filemode.normal_file"
|
||||
case "100755":
|
||||
return "git.filemode.executable_file"
|
||||
case "120000":
|
||||
return "git.filemode.symbolic_link"
|
||||
case "160000":
|
||||
return "git.filemode.submodule"
|
||||
default:
|
||||
return mode
|
||||
func (diffFile *DiffFile) TranslateDiffEntryMode(locale translation.Locale) string {
|
||||
entryModeTr := func(mode string) string {
|
||||
entryMode := git.ParseEntryMode(mode)
|
||||
switch {
|
||||
case entryMode.IsDir():
|
||||
return locale.TrString("git.filemode.directory")
|
||||
case entryMode.IsRegular():
|
||||
return locale.TrString("git.filemode.normal_file")
|
||||
case entryMode.IsExecutable():
|
||||
return locale.TrString("git.filemode.executable_file")
|
||||
case entryMode.IsLink():
|
||||
return locale.TrString("git.filemode.symbolic_link")
|
||||
case entryMode.IsSubModule():
|
||||
return locale.TrString("git.filemode.submodule")
|
||||
default:
|
||||
return mode
|
||||
}
|
||||
}
|
||||
|
||||
if diffFile.EntryMode != "" && diffFile.OldEntryMode != "" {
|
||||
oldMode := entryModeTr(diffFile.OldEntryMode)
|
||||
newMode := entryModeTr(diffFile.EntryMode)
|
||||
return locale.TrString("git.filemode.changed_filemode", oldMode, newMode)
|
||||
}
|
||||
if diffFile.EntryMode != "" {
|
||||
if entryMode := git.ParseEntryMode(diffFile.EntryMode); !entryMode.IsRegular() {
|
||||
return entryModeTr(diffFile.EntryMode)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type limitByteWriter struct {
|
||||
@@ -439,7 +582,7 @@ func getCommitFileLineCountAndLimitedContent(commit *git.Commit, filePath string
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
}
|
||||
w := &limitByteWriter{limit: MaxDiffHighlightEntireFileSize + 1}
|
||||
w := &limitByteWriter{limit: MaxFullFileHighlightSizeLimit + 1}
|
||||
lineCount, err = blob.GetBlobLineCount(w)
|
||||
if err != nil {
|
||||
return 0, nil
|
||||
@@ -473,6 +616,8 @@ func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, c
|
||||
sort.SliceStable(line.Comments, func(i, j int) bool {
|
||||
return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix
|
||||
})
|
||||
// Mark expand buttons that have comments in hidden lines
|
||||
FillHiddenCommentIDsForDiffLine(line, lineCommits)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -597,10 +742,10 @@ parsingLoop:
|
||||
strings.HasPrefix(line, "new mode "):
|
||||
|
||||
if strings.HasPrefix(line, "old mode ") {
|
||||
curFile.OldMode = prepareValue(line, "old mode ")
|
||||
curFile.OldEntryMode = prepareValue(line, "old mode ")
|
||||
}
|
||||
if strings.HasPrefix(line, "new mode ") {
|
||||
curFile.Mode = prepareValue(line, "new mode ")
|
||||
curFile.EntryMode = prepareValue(line, "new mode ")
|
||||
}
|
||||
if strings.HasSuffix(line, " 160000\n") {
|
||||
curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{}
|
||||
@@ -635,7 +780,7 @@ parsingLoop:
|
||||
curFile.Type = DiffFileAdd
|
||||
curFile.IsCreated = true
|
||||
if strings.HasPrefix(line, "new file mode ") {
|
||||
curFile.Mode = prepareValue(line, "new file mode ")
|
||||
curFile.EntryMode = prepareValue(line, "new file mode ")
|
||||
}
|
||||
if strings.HasSuffix(line, " 160000\n") {
|
||||
curFile.IsSubmodule, curFile.SubmoduleDiffInfo = true, &SubmoduleDiffInfo{}
|
||||
@@ -742,11 +887,11 @@ parsingLoop:
|
||||
if buffer.Len() == 0 {
|
||||
continue
|
||||
}
|
||||
charsetLabel, err := charset.DetectEncoding(buffer.Bytes())
|
||||
if charsetLabel != "UTF-8" && err == nil {
|
||||
encoding, _ := stdcharset.Lookup(charsetLabel)
|
||||
if encoding != nil {
|
||||
diffLineTypeDecoders[lineType] = encoding.NewDecoder()
|
||||
charsetLabel, _ := charset.DetectEncoding(buffer.Bytes())
|
||||
if charsetLabel != "UTF-8" {
|
||||
charsetEncoding, _ := stdcharset.Lookup(charsetLabel)
|
||||
if charsetEncoding != nil {
|
||||
diffLineTypeDecoders[lineType] = charsetEncoding.NewDecoder()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -795,6 +940,15 @@ func skipToNextDiffHead(input *bufio.Reader) (line string, err error) {
|
||||
return line, err
|
||||
}
|
||||
|
||||
func newDiffSectionForDiffFile(curFile *DiffFile) *DiffSection {
|
||||
return &DiffSection{
|
||||
language: &curFile.language,
|
||||
highlightLexer: &curFile.highlightRender,
|
||||
highlightedLeftLines: &curFile.highlightedLeftLines,
|
||||
highlightedRightLines: &curFile.highlightedRightLines,
|
||||
}
|
||||
}
|
||||
|
||||
func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio.Reader) (lineBytes []byte, isFragment bool, err error) {
|
||||
sb := strings.Builder{}
|
||||
|
||||
@@ -852,12 +1006,12 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
|
||||
line := sb.String()
|
||||
|
||||
// Create a new section to represent this hunk
|
||||
curSection = &DiffSection{file: curFile}
|
||||
curSection = newDiffSectionForDiffFile(curFile)
|
||||
lastLeftIdx = -1
|
||||
curFile.Sections = append(curFile.Sections, curSection)
|
||||
|
||||
// FIXME: the "-1" can't be right, these "line idx" are all 1-based, maybe there are other bugs that covers this bug.
|
||||
lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1)
|
||||
lineSectionInfo := newDiffLineSectionInfo(curFile, line, leftLine-1, rightLine-1)
|
||||
diffLine := &DiffLine{
|
||||
Type: DiffLineSection,
|
||||
Content: line,
|
||||
@@ -892,7 +1046,7 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
|
||||
rightLine++
|
||||
if curSection == nil {
|
||||
// Create a new section to represent this hunk
|
||||
curSection = &DiffSection{file: curFile}
|
||||
curSection = newDiffSectionForDiffFile(curFile)
|
||||
curFile.Sections = append(curFile.Sections, curSection)
|
||||
lastLeftIdx = -1
|
||||
}
|
||||
@@ -925,7 +1079,7 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
|
||||
}
|
||||
if curSection == nil {
|
||||
// Create a new section to represent this hunk
|
||||
curSection = &DiffSection{file: curFile}
|
||||
curSection = newDiffSectionForDiffFile(curFile)
|
||||
curFile.Sections = append(curFile.Sections, curSection)
|
||||
lastLeftIdx = -1
|
||||
}
|
||||
@@ -952,7 +1106,7 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
|
||||
lastLeftIdx = -1
|
||||
if curSection == nil {
|
||||
// Create a new section to represent this hunk
|
||||
curSection = &DiffSection{file: curFile}
|
||||
curSection = newDiffSectionForDiffFile(curFile)
|
||||
curFile.Sections = append(curFile.Sections, curSection)
|
||||
}
|
||||
curSection.Lines = append(curSection.Lines, diffLine)
|
||||
@@ -1132,8 +1286,9 @@ func getDiffBasic(ctx context.Context, gitRepo *git.Repository, opts *DiffOption
|
||||
}
|
||||
|
||||
cmdDiff := gitcmd.NewCommand().
|
||||
AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M").
|
||||
AddArguments(opts.WhitespaceBehavior...)
|
||||
AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/").
|
||||
AddArguments(opts.WhitespaceBehavior...).
|
||||
AddOptionFormat("--find-renames=%s", setting.Git.DiffRenameSimilarityThreshold)
|
||||
|
||||
// In git 2.31, git diff learned --skip-to which we can use to shortcut skip to file
|
||||
// so if we are using at least this version of git we don't have to tell ParsePatch to do
|
||||
@@ -1150,24 +1305,14 @@ func getDiffBasic(ctx context.Context, gitRepo *git.Repository, opts *DiffOption
|
||||
cmdCtx, cmdCancel := context.WithCancel(ctx)
|
||||
defer cmdCancel()
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
defer func() {
|
||||
_ = reader.Close()
|
||||
_ = writer.Close()
|
||||
}()
|
||||
|
||||
reader, readerClose := cmdDiff.MakeStdoutPipe()
|
||||
defer readerClose()
|
||||
go func() {
|
||||
stderr := &bytes.Buffer{}
|
||||
if err := cmdDiff.Run(cmdCtx, &gitcmd.RunOpts{
|
||||
Timeout: time.Duration(setting.Git.Timeout.Default) * time.Second,
|
||||
Dir: repoPath,
|
||||
Stdout: writer,
|
||||
Stderr: stderr,
|
||||
}); err != nil && !git.IsErrCanceledOrKilled(err) {
|
||||
log.Error("error during GetDiff(git diff dir: %s): %v, stderr: %s", repoPath, err, stderr.String())
|
||||
if err := cmdDiff.
|
||||
WithDir(repoPath).
|
||||
RunWithStderr(cmdCtx); err != nil && !gitcmd.IsErrorCanceledOrKilled(err) {
|
||||
log.Error("error during GetDiff(git diff dir: %s): %v", repoPath, err)
|
||||
}
|
||||
|
||||
_ = writer.Close()
|
||||
}()
|
||||
|
||||
diff, err := ParsePatch(cmdCtx, opts.MaxLines, opts.MaxLineCharacters, opts.MaxFiles, reader, parsePatchSkipToFile)
|
||||
@@ -1191,6 +1336,8 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
|
||||
return nil, err
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
checker, err := attribute.NewBatchChecker(gitRepo, opts.AfterCommitID, []string{attribute.LinguistVendored, attribute.LinguistGenerated, attribute.LinguistLanguage, attribute.GitlabLanguage, attribute.Diff})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1206,7 +1353,7 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
|
||||
isVendored, isGenerated = attrs.GetVendored(), attrs.GetGenerated()
|
||||
language := attrs.GetLanguage()
|
||||
if language.Has() {
|
||||
diffFile.Language = language.Value()
|
||||
diffFile.language.value = language.Value()
|
||||
}
|
||||
attrDiff = attrs.Get(attribute.Diff).ToString()
|
||||
}
|
||||
@@ -1230,13 +1377,14 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
|
||||
diffFile.Sections = append(diffFile.Sections, tailSection)
|
||||
}
|
||||
|
||||
shouldFullFileHighlight := !setting.Git.DisableDiffHighlight && attrDiff.Value() == ""
|
||||
shouldFullFileHighlight := attrDiff.Value() == "" // only do highlight if no custom diff command
|
||||
shouldFullFileHighlight = shouldFullFileHighlight && time.Since(startTime) < MaxFullFileHighlightTimeLimit
|
||||
if shouldFullFileHighlight {
|
||||
if limitedContent.LeftContent != nil && limitedContent.LeftContent.buf.Len() < MaxDiffHighlightEntireFileSize {
|
||||
diffFile.highlightedLeftLines = highlightCodeLines(diffFile, true /* left */, limitedContent.LeftContent.buf.Bytes())
|
||||
if limitedContent.LeftContent != nil {
|
||||
diffFile.highlightedLeftLines.value = highlightCodeLinesForDiffFile(diffFile, true /* left */, limitedContent.LeftContent.buf.Bytes())
|
||||
}
|
||||
if limitedContent.RightContent != nil && limitedContent.RightContent.buf.Len() < MaxDiffHighlightEntireFileSize {
|
||||
diffFile.highlightedRightLines = highlightCodeLines(diffFile, false /* right */, limitedContent.RightContent.buf.Bytes())
|
||||
if limitedContent.RightContent != nil {
|
||||
diffFile.highlightedRightLines.value = highlightCodeLinesForDiffFile(diffFile, false /* right */, limitedContent.RightContent.buf.Bytes())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1244,38 +1392,27 @@ func GetDiffForRender(ctx context.Context, repoLink string, gitRepo *git.Reposit
|
||||
return diff, nil
|
||||
}
|
||||
|
||||
func splitHighlightLines(buf []byte) (ret [][]byte) {
|
||||
lineCount := bytes.Count(buf, []byte("\n")) + 1
|
||||
ret = make([][]byte, 0, lineCount)
|
||||
nlTagClose := []byte("\n</")
|
||||
for {
|
||||
pos := bytes.IndexByte(buf, '\n')
|
||||
if pos == -1 {
|
||||
ret = append(ret, buf)
|
||||
return ret
|
||||
}
|
||||
// Chroma highlighting output sometimes have "</span>" right after \n, sometimes before.
|
||||
// * "<span>text\n</span>"
|
||||
// * "<span>text</span>\n"
|
||||
if bytes.HasPrefix(buf[pos:], nlTagClose) {
|
||||
pos1 := bytes.IndexByte(buf[pos:], '>')
|
||||
if pos1 != -1 {
|
||||
pos += pos1
|
||||
}
|
||||
}
|
||||
ret = append(ret, buf[:pos+1])
|
||||
buf = buf[pos+1:]
|
||||
}
|
||||
func FillDiffFileHighlightLinesByContent(diffFile *DiffFile, left, right []byte) {
|
||||
diffFile.highlightedLeftLines.value = highlightCodeLinesForDiffFile(diffFile, true /* left */, left)
|
||||
diffFile.highlightedRightLines.value = highlightCodeLinesForDiffFile(diffFile, false /* right */, right)
|
||||
}
|
||||
|
||||
func highlightCodeLines(diffFile *DiffFile, isLeft bool, rawContent []byte) map[int]template.HTML {
|
||||
contentUTF8, _ := charset.ToUTF8(rawContent, charset.ConvertOpts{})
|
||||
content := util.UnsafeBytesToString(contentUTF8)
|
||||
highlightedNewContent, _ := highlight.Code(diffFile.Name, diffFile.Language, content)
|
||||
splitLines := splitHighlightLines([]byte(highlightedNewContent))
|
||||
lines := make(map[int]template.HTML, len(splitLines))
|
||||
func highlightCodeLinesForDiffFile(diffFile *DiffFile, isLeft bool, rawContent []byte) map[int]template.HTML {
|
||||
return highlightCodeLines(diffFile.Name, diffFile.language.value, diffFile.Sections, isLeft, rawContent)
|
||||
}
|
||||
|
||||
func highlightCodeLines(name, lang string, sections []*DiffSection, isLeft bool, rawContent []byte) map[int]template.HTML {
|
||||
if setting.Git.DisableDiffHighlight || len(rawContent) > MaxFullFileHighlightSizeLimit {
|
||||
return nil
|
||||
}
|
||||
|
||||
content := util.UnsafeBytesToString(charset.ToUTF8(rawContent, charset.ConvertOpts{}))
|
||||
lexer := highlight.DetectChromaLexerByFileName(name, lang)
|
||||
highlightedNewContent := highlight.RenderCodeByLexer(lexer, content)
|
||||
unsafeLines := highlight.UnsafeSplitHighlightedLines(highlightedNewContent)
|
||||
lines := make(map[int]template.HTML, len(unsafeLines))
|
||||
// only save the highlighted lines we need, but not the whole file, to save memory
|
||||
for _, sec := range diffFile.Sections {
|
||||
for _, sec := range sections {
|
||||
for _, ln := range sec.Lines {
|
||||
lineIdx := ln.LeftIdx
|
||||
if !isLeft {
|
||||
@@ -1283,8 +1420,8 @@ func highlightCodeLines(diffFile *DiffFile, isLeft bool, rawContent []byte) map[
|
||||
}
|
||||
if lineIdx >= 1 {
|
||||
idx := lineIdx - 1
|
||||
if idx < len(splitLines) {
|
||||
lines[idx] = template.HTML(splitLines[idx])
|
||||
if idx < len(unsafeLines) {
|
||||
lines[idx] = template.HTML(util.UnsafeBytesToString(unsafeLines[idx]))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1296,9 +1433,7 @@ type DiffShortStat struct {
|
||||
NumFiles, TotalAddition, TotalDeletion int
|
||||
}
|
||||
|
||||
func GetDiffShortStat(gitRepo *git.Repository, beforeCommitID, afterCommitID string) (*DiffShortStat, error) {
|
||||
repoPath := gitRepo.Path
|
||||
|
||||
func GetDiffShortStat(ctx context.Context, repoStorage gitrepo.Repository, gitRepo *git.Repository, beforeCommitID, afterCommitID string) (*DiffShortStat, error) {
|
||||
afterCommit, err := gitRepo.GetCommit(afterCommitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1310,7 +1445,7 @@ func GetDiffShortStat(gitRepo *git.Repository, beforeCommitID, afterCommitID str
|
||||
}
|
||||
|
||||
diff := &DiffShortStat{}
|
||||
diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStatByCmdArgs(gitRepo.Ctx, repoPath, nil, actualBeforeCommitID.String(), afterCommitID)
|
||||
diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = gitrepo.GetDiffShortStatByCmdArgs(ctx, repoStorage, nil, actualBeforeCommitID.String(), afterCommitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1342,38 +1477,40 @@ func SyncUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.
|
||||
if errIgnored != nil {
|
||||
log.Error("Could not get changed files between %s and %s for pull request %d in repo with path %s. Assuming no changes. Error: %w", review.CommitSHA, latestCommit, pull.Index, gitRepo.Path, err)
|
||||
}
|
||||
changedFilesSet := make(map[string]struct{}, len(changedFiles))
|
||||
for _, changedFile := range changedFiles {
|
||||
changedFilesSet[changedFile] = struct{}{}
|
||||
}
|
||||
|
||||
filesChangedSinceLastDiff := make(map[string]pull_model.ViewedState)
|
||||
outer:
|
||||
for _, diffFile := range diff.Files {
|
||||
fileViewedState := review.UpdatedFiles[diffFile.GetDiffFileName()]
|
||||
|
||||
// Check whether it was previously detected that the file has changed since the last review
|
||||
if fileViewedState == pull_model.HasChanged {
|
||||
diffFile.HasChangedSinceLastReview = true
|
||||
continue
|
||||
}
|
||||
|
||||
filename := diffFile.GetDiffFileName()
|
||||
fileViewedState := review.UpdatedFiles[filename]
|
||||
|
||||
// Check explicitly whether the file has changed since the last review
|
||||
for _, changedFile := range changedFiles {
|
||||
diffFile.HasChangedSinceLastReview = filename == changedFile
|
||||
if diffFile.HasChangedSinceLastReview {
|
||||
filesChangedSinceLastDiff[filename] = pull_model.HasChanged
|
||||
continue outer // We don't want to check if the file is viewed here as that would fold the file, which is in this case unwanted
|
||||
}
|
||||
}
|
||||
// Check whether the file has already been viewed
|
||||
if fileViewedState == pull_model.Viewed {
|
||||
if fileViewedState == pull_model.HasChanged { // Check whether it was previously detected that the file has changed since the last review
|
||||
diffFile.HasChangedSinceLastReview = true
|
||||
delete(changedFilesSet, filename)
|
||||
} else if _, ok := changedFilesSet[filename]; ok { // Check explicitly whether the file has changed since the last review
|
||||
diffFile.HasChangedSinceLastReview = true
|
||||
filesChangedSinceLastDiff[filename] = pull_model.HasChanged
|
||||
delete(changedFilesSet, filename)
|
||||
} else if fileViewedState == pull_model.Viewed { // Check whether the file has already been viewed
|
||||
diffFile.IsViewed = true
|
||||
}
|
||||
}
|
||||
|
||||
// All changed files still present at this point aren't part of the diff anymore, this occurs
|
||||
// when a file was modified in a previous commit of the diff and the modification got reverted afterwards.
|
||||
// Marking the files as unviewed to prevent errors where a non-existing file has a view state
|
||||
for changedFile := range changedFilesSet {
|
||||
if _, ok := review.UpdatedFiles[changedFile]; ok {
|
||||
filesChangedSinceLastDiff[changedFile] = pull_model.Unviewed
|
||||
}
|
||||
}
|
||||
|
||||
if len(filesChangedSinceLastDiff) > 0 {
|
||||
// Explicitly store files that have changed in the database, if any is present at all.
|
||||
// This has the benefit that the "Has Changed" attribute will be present as long as the user does not explicitly mark this file as viewed, so it will even survive a page reload after marking another file as viewed.
|
||||
// On the other hand, this means that even if a commit reverting an unseen change is committed, the file will still be seen as changed.
|
||||
updatedReview, err := pull_model.UpdateReviewState(ctx, review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff)
|
||||
if err != nil {
|
||||
log.Warn("Could not update review for user %d, pull %d, commit %s and the changed files %v: %v", review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff, err)
|
||||
@@ -1404,6 +1541,75 @@ func CommentAsDiff(ctx context.Context, c *issues_model.Comment) (*Diff, error)
|
||||
return diff, nil
|
||||
}
|
||||
|
||||
// GeneratePatchForUnchangedLine creates a patch showing code context for an unchanged line
|
||||
func GeneratePatchForUnchangedLine(gitRepo *git.Repository, commitID, treePath string, line int64, contextLines int) (string, error) {
|
||||
commit, err := gitRepo.GetCommit(commitID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("GetCommit: %w", err)
|
||||
}
|
||||
|
||||
entry, err := commit.GetTreeEntryByPath(treePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("GetTreeEntryByPath: %w", err)
|
||||
}
|
||||
|
||||
blob := entry.Blob()
|
||||
dataRc, err := blob.DataAsync()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("DataAsync: %w", err)
|
||||
}
|
||||
defer dataRc.Close()
|
||||
|
||||
return generatePatchForUnchangedLineFromReader(dataRc, treePath, line, contextLines)
|
||||
}
|
||||
|
||||
// generatePatchForUnchangedLineFromReader is the testable core logic that generates a patch from a reader
|
||||
func generatePatchForUnchangedLineFromReader(reader io.Reader, treePath string, line int64, contextLines int) (string, error) {
|
||||
// Calculate line range (commented line + lines above it)
|
||||
commentLine := int(line)
|
||||
if line < 0 {
|
||||
commentLine = int(-line)
|
||||
}
|
||||
startLine := max(commentLine-contextLines, 1)
|
||||
endLine := commentLine
|
||||
|
||||
// Read only the needed lines efficiently
|
||||
scanner := bufio.NewScanner(reader)
|
||||
currentLine := 0
|
||||
var lines []string
|
||||
for scanner.Scan() {
|
||||
currentLine++
|
||||
if currentLine >= startLine && currentLine <= endLine {
|
||||
lines = append(lines, scanner.Text())
|
||||
}
|
||||
if currentLine > endLine {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", fmt.Errorf("scanner error: %w", err)
|
||||
}
|
||||
|
||||
if len(lines) == 0 {
|
||||
return "", fmt.Errorf("no lines found in range %d-%d", startLine, endLine)
|
||||
}
|
||||
|
||||
// Generate synthetic patch
|
||||
var patchBuilder strings.Builder
|
||||
fmt.Fprintf(&patchBuilder, "diff --git a/%s b/%s\n", treePath, treePath)
|
||||
fmt.Fprintf(&patchBuilder, "--- a/%s\n", treePath)
|
||||
fmt.Fprintf(&patchBuilder, "+++ b/%s\n", treePath)
|
||||
fmt.Fprintf(&patchBuilder, "@@ -%d,%d +%d,%d @@\n", startLine, len(lines), startLine, len(lines))
|
||||
|
||||
for _, lineContent := range lines {
|
||||
patchBuilder.WriteString(" ")
|
||||
patchBuilder.WriteString(lineContent)
|
||||
patchBuilder.WriteString("\n")
|
||||
}
|
||||
|
||||
return patchBuilder.String(), nil
|
||||
}
|
||||
|
||||
// CommentMustAsDiff executes AsDiff and logs the error instead of returning
|
||||
func CommentMustAsDiff(ctx context.Context, c *issues_model.Comment) *Diff {
|
||||
if c == nil {
|
||||
|
||||
Reference in New Issue
Block a user