Skip to content

fix highlight problem of git diff #24336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,12 @@ ROUTER = console
;; Disables highlight of added and removed changes
;DISABLE_DIFF_HIGHLIGHT = false
;;
;; max size of one file that can be full highlight on git diff ui.
;; if the file is too big, it will be renderd line by line,
;; sometimes it will lead a not good highlight result.
;;
;MAX_DIFF_HIGHLIGHT_FILE_SIZE = 5 * 1024
;;
;; Max number of lines allowed in a single file in diff view
;MAX_GIT_DIFF_LINES = 1000
;;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,7 @@ Default templates for project boards:
- `HOME_PATH`: **%(APP_DATA_PATH)s/home**: The HOME directory for Git.
This directory will be used to contain the `.gitconfig` and possible `.gnupg` directories that Gitea's git calls will use. If you can confirm Gitea is the only application running in this environment, you can set it to the normal home directory for Gitea user.
- `DISABLE_DIFF_HIGHLIGHT`: **false**: Disables highlight of added and removed changes.
- `MAX_DIFF_HIGHLIGHT_FILE_SIZE`: **5120** max size of one file that can be full highlight on git diff ui.
- `MAX_GIT_DIFF_LINES`: **1000**: Max number of lines allowed of a single file in diff view.
- `MAX_GIT_DIFF_LINE_CHARACTERS`: **5000**: Max character count per line highlighted in diff view.
- `MAX_GIT_DIFF_FILES`: **100**: Max number of files shown in diff view.
Expand Down
2 changes: 2 additions & 0 deletions modules/setting/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ var Git = struct {
Pull int
GC int `ini:"GC"`
} `ini:"git.timeout"`
MaxDiffHighlightFileSize int64
}{
Reflog: struct {
Enabled bool
Expand Down Expand Up @@ -76,6 +77,7 @@ var Git = struct {
Pull: 300,
GC: 60,
},
MaxDiffHighlightFileSize: 5 * 1024,
}

func loadGitFrom(rootCfg ConfigProvider) {
Expand Down
36 changes: 34 additions & 2 deletions routers/web/repo/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package repo

import (
"bufio"
"bytes"
gocontext "context"
"encoding/csv"
"errors"
Expand All @@ -27,6 +28,7 @@ import (
"code.gitea.io/gitea/modules/context"
csv_module "code.gitea.io/gitea/modules/csv"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/highlight"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/setting"
Expand Down Expand Up @@ -929,8 +931,32 @@ func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chu
if err != nil {
return nil, err
}
defer reader.Close()
scanner := bufio.NewScanner(reader)

var (
scanner *bufio.Scanner
highlightedContent []string
)

if blob.Size() >= setting.Git.MaxDiffHighlightFileSize {
defer reader.Close()
scanner = bufio.NewScanner(reader)
} else {
content, err := io.ReadAll(reader)
_ = reader.Close()
if err != nil {
log.Error("io.ReadAll: %v", err)
return nil, err
}

highlightedContent, _, err = highlight.File(filePath, "", content)
if err != nil {
log.Error("highlight.File: %v", err)
return nil, err
}

scanner = bufio.NewScanner(bytes.NewBuffer(content))
}

var diffLines []*gitdiff.DiffLine
for line := 0; line < idxRight+chunkSize; line++ {
if ok := scanner.Scan(); !ok {
Expand All @@ -946,6 +972,12 @@ func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chu
Type: gitdiff.DiffLinePlain,
Content: " " + lineText,
}

if highlightedContent != nil {
diffLine.HighlightContent = highlightedContent[line]
diffLine.HasHighlightContent = true
}

diffLines = append(diffLines, diffLine)
}
return diffLines, nil
Expand Down
164 changes: 150 additions & 14 deletions services/gitdiff/gitdiff.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,15 @@ const (

// DiffLine represents a line difference in a DiffSection.
type DiffLine struct {
LeftIdx int
RightIdx int
Match int
Type DiffLineType
Content string
Comments []*issues_model.Comment
SectionInfo *DiffLineSectionInfo
LeftIdx int
RightIdx int
Match int
Type DiffLineType
Content string
Comments []*issues_model.Comment
SectionInfo *DiffLineSectionInfo
HighlightContent string
HasHighlightContent bool
}

// DiffLineSectionInfo represents diff line section meta data
Expand Down Expand Up @@ -292,8 +294,8 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, loc

var (
compareDiffLine *DiffLine
diff1 string
diff2 string
diff1 *DiffLine
diff2 *DiffLine
)

language := ""
Expand All @@ -308,26 +310,41 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, loc
case DiffLineAdd:
compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
if compareDiffLine == nil {
if diffLine.HasHighlightContent {
status, content := charset.EscapeControlHTML(diffLine.HighlightContent, locale)
return DiffInline{EscapeStatus: status, Content: template.HTML(content)}
}

return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale)
}
diff1 = compareDiffLine.Content
diff2 = diffLine.Content
diff1 = compareDiffLine
diff2 = diffLine
case DiffLineDel:
compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
if compareDiffLine == nil {
if diffLine.HasHighlightContent {
status, content := charset.EscapeControlHTML(diffLine.HighlightContent, locale)
return DiffInline{EscapeStatus: status, Content: template.HTML(content)}
}

return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale)
}
diff1 = diffLine.Content
diff2 = compareDiffLine.Content
diff1 = diffLine
diff2 = compareDiffLine
default:
if strings.IndexByte(" +-", diffLine.Content[0]) > -1 {
if diffLine.HasHighlightContent {
status, content := charset.EscapeControlHTML(diffLine.HighlightContent, locale)
return DiffInline{EscapeStatus: status, Content: template.HTML(content)}
}

return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale)
}
return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content, locale)
}

hcd := newHighlightCodeDiff()
diffRecord := hcd.diffWithHighlight(diffSection.FileName, language, diff1[1:], diff2[1:])
diffRecord := hcd.diffWithHighlight(diffSection.FileName, language, diff1.Content[1:], diff1.HighlightContent, diff2.Content[1:], diff2.HighlightContent)
// it seems that Gitea doesn't need the line wrapper of Chroma, so do not add them back
// if the line wrappers are still needed in the future, it can be added back by "diffToHTML(hcd.lineWrapperTags. ...)"
diffHTML := diffToHTML(nil, diffRecord, diffLine.Type)
Expand Down Expand Up @@ -365,6 +382,115 @@ func (diffFile *DiffFile) GetType() int {
return int(diffFile.Type)
}

func (diffFile *DiffFile) FullFileHiglight(beforeCommit, commit *git.Commit, beforeContent, content []byte) {
if diffFile.IsBin {
return
}

preRenderAddOrDeletedFile := func(path, lang string) ([]string, string) {
content := ""

if len(diffFile.Sections) != 1 {
return nil, ""
}

for index, diffLine := range diffFile.Sections[0].Lines {
if diffLine.Type == DiffLineSection && index == 0 {
continue
}

if diffLine.Type != DiffLineAdd && diffLine.Type != DiffLineDel {
return nil, ""
}

content += diffLine.Content[1:] + "\n"
}

highlightedContent, lang, err := highlight.File(path, lang, []byte(content))
if err != nil {
log.Error("highlight.File: %v", err)
return nil, ""
}

return highlightedContent, lang
}

loadCommitFileContent := func(commit *git.Commit, path, lang string, content []byte) ([]string, string) {
if commit == nil && len(content) == 0 {
return nil, ""
}

if len(content) == 0 {
entry, err := commit.GetTreeEntryByPath(path)
if err != nil {
log.Error("GetTreeEntryByPath: %v", err)
return nil, ""
}

if entry.Blob().Size() >= setting.Git.MaxDiffHighlightFileSize {
return nil, ""
}

f, err := entry.Blob().DataAsync()
if err != nil {
log.Error("Blob.DataAsync: %v", err)
return nil, ""
}

content, err = io.ReadAll(f)
_ = f.Close()
if err != nil {
log.Error("io.ReadAll: %v", err)
return nil, ""
}
}

highlightedContent, lang, err := highlight.File(path, lang, content)
if err != nil {
log.Error("highlight.File: %v", err)
return nil, ""
}

return highlightedContent, lang
}

var (
oldContent []string
newContent []string
)

if diffFile.Type == DiffFileDel {
oldContent, diffFile.Language = preRenderAddOrDeletedFile(diffFile.OldName, diffFile.Language)
} else if diffFile.Type == DiffFileAdd {
newContent, diffFile.Language = preRenderAddOrDeletedFile(diffFile.Name, diffFile.Language)
} else if diffFile.Type == DiffFileChange {
oldContent, _ = loadCommitFileContent(beforeCommit, diffFile.OldName, diffFile.Language, beforeContent)
newContent, diffFile.Language = loadCommitFileContent(commit, diffFile.Name, diffFile.Language, content)
} else {
return
}

for _, diffSection := range diffFile.Sections {
for _, diffLine := range diffSection.Lines {
switch diffLine.Type {
case DiffLineAdd:
fallthrough
case DiffLinePlain:
if diffLine.RightIdx > 0 && diffLine.RightIdx <= len(newContent) {
diffLine.HighlightContent = newContent[diffLine.RightIdx-1]
diffLine.HasHighlightContent = true
}

case DiffLineDel:
if diffLine.LeftIdx > 0 && diffLine.LeftIdx <= len(oldContent) {
diffLine.HighlightContent = oldContent[diffLine.LeftIdx-1]
diffLine.HasHighlightContent = true
}
}
}
}
}

// GetTailSection creates a fake DiffLineSection if the last section is not the end of the file
func (diffFile *DiffFile) GetTailSection(gitRepo *git.Repository, leftCommitID, rightCommitID string) *DiffSection {
if len(diffFile.Sections) == 0 || diffFile.Type != DiffFileChange || diffFile.IsBin || diffFile.IsLFSFile {
Expand Down Expand Up @@ -1071,6 +1197,8 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff
return nil, err
}

var beforeCommit *git.Commit

cmdDiff := git.NewCommand(gitRepo.Ctx)
if (len(opts.BeforeCommitID) == 0 || opts.BeforeCommitID == git.EmptySHA) && commit.ParentCount() == 0 {
cmdDiff.AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M").
Expand All @@ -1082,6 +1210,12 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff
if len(actualBeforeCommitID) == 0 {
parentCommit, _ := commit.Parent(0)
actualBeforeCommitID = parentCommit.ID.String()
beforeCommit = parentCommit
} else {
beforeCommit, err = gitRepo.GetCommit(actualBeforeCommitID)
if err != nil {
return nil, err
}
}

cmdDiff.AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M").
Expand Down Expand Up @@ -1172,6 +1306,8 @@ func GetDiff(gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff
if tailSection != nil {
diffFile.Sections = append(diffFile.Sections, tailSection)
}

diffFile.FullFileHiglight(beforeCommit, commit, nil, nil)
}

separator := "..."
Expand Down
11 changes: 8 additions & 3 deletions services/gitdiff/highlightdiff.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,17 @@ func (hcd *highlightCodeDiff) collectUsedRunes(code string) {
}
}

func (hcd *highlightCodeDiff) diffWithHighlight(filename, language, codeA, codeB string) []diffmatchpatch.Diff {
func (hcd *highlightCodeDiff) diffWithHighlight(filename, language, codeA, highlightCodeA, codeB, highlightCodeB string) []diffmatchpatch.Diff {
hcd.collectUsedRunes(codeA)
hcd.collectUsedRunes(codeB)

highlightCodeA, _ := highlight.Code(filename, language, codeA)
highlightCodeB, _ := highlight.Code(filename, language, codeB)
if len(highlightCodeA) == 0 {
highlightCodeA, _ = highlight.Code(filename, language, codeA)
}

if len(highlightCodeB) == 0 {
highlightCodeB, _ = highlight.Code(filename, language, codeB)
}

highlightCodeA = hcd.convertToPlaceholders(highlightCodeA)
highlightCodeB = hcd.convertToPlaceholders(highlightCodeB)
Expand Down
Loading