0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-02-23 23:43:43 +01:00
gitea/services/gitdiff/gitdiff_excerpt.go
silverwind dbb8312ff3
DRY blob excerpt: unified single/batch path, single-pass highlighting
- Merge ExcerptBlob and excerptBlobBatch into a single unified flow
  that handles both single and batch requests without duplicated code
- Extract parseBatchBlobExcerptOptions to DRY the repetitive splitInts calls
- Refactor BuildBlobExcerptDiffSection to separate section building from
  highlighting, enabling BuildBlobExcerptDiffSections (plural) to highlight
  the file content only once for all sections instead of N times
- Add blob size limit check (MaxDisplayFileSize) before io.ReadAll to
  prevent OOM on large files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 13:08:24 +01:00

161 lines
5.0 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package gitdiff
import (
"bufio"
"bytes"
"fmt"
"html/template"
"io"
"code.gitea.io/gitea/modules/setting"
"github.com/alecthomas/chroma/v2"
)
type BlobExcerptOptions struct {
LastLeft int
LastRight int
LeftIndex int
RightIndex int
LeftHunkSize int
RightHunkSize int
Direction string
Language string
}
// fillExcerptLines reads from reader and populates section.Lines.
// It returns the accumulated content buffer for later highlighting.
func fillExcerptLines(section *DiffSection, reader io.Reader, idxLeft, idxRight, chunkSize int) ([]byte, error) {
buf := &bytes.Buffer{}
scanner := bufio.NewScanner(reader)
var diffLines []*DiffLine
for line := 0; line < idxRight+chunkSize; line++ {
if ok := scanner.Scan(); !ok {
break
}
lineText := scanner.Text()
if buf.Len()+len(lineText) < int(setting.UI.MaxDisplayFileSize) {
buf.WriteString(lineText)
buf.WriteByte('\n')
}
if line < idxRight {
continue
}
diffLine := &DiffLine{
LeftIdx: idxLeft + (line - idxRight) + 1,
RightIdx: line + 1,
Type: DiffLinePlain,
Content: " " + lineText,
}
diffLines = append(diffLines, diffLine)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("fillExcerptLines scan: %w", err)
}
section.Lines = diffLines
return buf.Bytes(), nil
}
// buildExcerptDiffSection builds a single excerpt section without highlighting.
// It returns the section and the accumulated content buffer.
func buildExcerptDiffSection(filePath string, reader io.Reader, opts BlobExcerptOptions) (*DiffSection, []byte, error) {
lastLeft, lastRight, idxLeft, idxRight := opts.LastLeft, opts.LastRight, opts.LeftIndex, opts.RightIndex
leftHunkSize, rightHunkSize, direction := opts.LeftHunkSize, opts.RightHunkSize, opts.Direction
chunkSize := BlobExcerptChunkSize
section := &DiffSection{
language: &diffVarMutable[string]{value: opts.Language},
highlightLexer: &diffVarMutable[chroma.Lexer]{},
highlightedLeftLines: &diffVarMutable[map[int]template.HTML]{},
highlightedRightLines: &diffVarMutable[map[int]template.HTML]{},
FileName: filePath,
}
var bufContent []byte
var err error
if direction == "up" && (idxLeft-lastLeft) > chunkSize {
idxLeft -= chunkSize
idxRight -= chunkSize
leftHunkSize += chunkSize
rightHunkSize += chunkSize
bufContent, err = fillExcerptLines(section, reader, idxLeft-1, idxRight-1, chunkSize)
} else if direction == "down" && (idxLeft-lastLeft) > chunkSize {
bufContent, err = fillExcerptLines(section, reader, lastLeft, lastRight, chunkSize)
lastLeft += chunkSize
lastRight += chunkSize
} else {
offset := -1
if direction == "down" {
offset = 0
}
bufContent, err = fillExcerptLines(section, reader, lastLeft, lastRight, idxRight-lastRight+offset)
leftHunkSize = 0
rightHunkSize = 0
idxLeft = lastLeft
idxRight = lastRight
}
if err != nil {
return nil, nil, err
}
newLineSection := &DiffLine{
Type: DiffLineSection,
SectionInfo: &DiffLineSectionInfo{
language: &diffVarMutable[string]{value: opts.Language},
Path: filePath,
LastLeftIdx: lastLeft,
LastRightIdx: lastRight,
LeftIdx: idxLeft,
RightIdx: idxRight,
LeftHunkSize: leftHunkSize,
RightHunkSize: rightHunkSize,
},
}
if newLineSection.GetExpandDirection() != "" {
newLineSection.Content = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize)
switch direction {
case "up":
section.Lines = append([]*DiffLine{newLineSection}, section.Lines...)
case "down":
section.Lines = append(section.Lines, newLineSection)
}
}
return section, bufContent, nil
}
// BuildBlobExcerptDiffSection builds a single excerpt section with highlighting.
func BuildBlobExcerptDiffSection(filePath string, reader io.Reader, opts BlobExcerptOptions) (*DiffSection, error) {
section, bufContent, err := buildExcerptDiffSection(filePath, reader, opts)
if err != nil {
return nil, err
}
// DiffLinePlain always uses right lines
section.highlightedRightLines.value = highlightCodeLines(filePath, opts.Language, []*DiffSection{section}, false /* right */, bufContent)
return section, nil
}
// BuildBlobExcerptDiffSections builds multiple excerpt sections from the same file content,
// highlighting the content only once for all sections.
func BuildBlobExcerptDiffSections(filePath string, content []byte, optsList []BlobExcerptOptions) ([]*DiffSection, error) {
sections := make([]*DiffSection, len(optsList))
for i, opts := range optsList {
section, _, err := buildExcerptDiffSection(filePath, bytes.NewReader(content), opts)
if err != nil {
return nil, err
}
sections[i] = section
}
// Highlight once for all sections
if len(optsList) > 0 {
highlighted := highlightCodeLines(filePath, optsList[0].Language, sections, false /* right */, content)
for _, section := range sections {
section.highlightedRightLines.value = highlighted
}
}
return sections, nil
}