]> source.dussan.org Git - gitea.git/commitdiff
Add .gitattribute assisted language detection to blame, diff and render (#17590)
authorzeripath <art27@cantab.net>
Wed, 17 Nov 2021 20:37:00 +0000 (20:37 +0000)
committerGitHub <noreply@github.com>
Wed, 17 Nov 2021 20:37:00 +0000 (20:37 +0000)
Use check attribute code to check the assigned language of a file and send that in to
chroma as a hint for the language of the file.

Signed-off-by: Andrew Thornton <art27@cantab.net>
13 files changed:
docs/content/doc/advanced/config-cheat-sheet.en-us.md
modules/git/repo_attribute.go
modules/git/repo_index.go
modules/git/repo_language_stats_gogit.go
modules/git/repo_language_stats_nogogit.go
modules/highlight/highlight.go
modules/highlight/highlight_test.go
modules/indexer/code/search.go
modules/repofiles/diff_test.go
routers/web/repo/blame.go
routers/web/repo/view.go
services/gitdiff/gitdiff.go
services/gitdiff/gitdiff_test.go

index 3b5d9213df6254bd4b57c377d2240922548c5d2d..a087b253e99ace665febaf0779b46c60415ac4d3 100644 (file)
@@ -982,6 +982,14 @@ Multiple sanitisation rules can be defined by adding unique subsections, e.g. `[
 To apply a sanitisation rules only for a specify external renderer they must use the renderer name, e.g. `[markup.sanitizer.asciidoc.rule-1]`.
 If the rule is defined above the renderer ini section or the name does not match a renderer it is applied to every renderer.
 
+## Highlight Mappings (`highlight.mapping`)
+
+- `file_extension e.g. .toml`: **language e.g. ini**. File extension to language mapping overrides.
+
+- Gitea will highlight files using the `linguist-language` or `gitlab-language` attribute from the `.gitattributes` file
+if available. If this is not set or the language is unavailable, the file extension will be looked up
+in this mapping or the filetype using heuristics.
+
 ## Time (`time`)
 
 - `FORMAT`: Time format to display on UI. i.e. RFC1123 or 2006-01-02 15:04:05
index aace64425388adb5d49e3a841640b0de08288fa7..88fb7810a663742503539833051e8ff3c265bc65 100644 (file)
@@ -22,6 +22,8 @@ type CheckAttributeOpts struct {
        AllAttributes bool
        Attributes    []string
        Filenames     []string
+       IndexFile     string
+       WorkTree      string
 }
 
 // CheckAttribute return the Blame object of file
@@ -31,6 +33,19 @@ func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[
                return nil, fmt.Errorf("git version missing: %v", err)
        }
 
+       env := []string{}
+
+       if len(opts.IndexFile) > 0 && CheckGitVersionAtLeast("1.7.8") == nil {
+               env = append(env, "GIT_INDEX_FILE="+opts.IndexFile)
+       }
+       if len(opts.WorkTree) > 0 && CheckGitVersionAtLeast("1.7.8") == nil {
+               env = append(env, "GIT_WORK_TREE="+opts.WorkTree)
+       }
+
+       if len(env) > 0 {
+               env = append(os.Environ(), env...)
+       }
+
        stdOut := new(bytes.Buffer)
        stdErr := new(bytes.Buffer)
 
@@ -61,7 +76,7 @@ func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[
 
        cmd := NewCommand(cmdArgs...)
 
-       if err := cmd.RunInDirPipeline(repo.Path, stdOut, stdErr); err != nil {
+       if err := cmd.RunInDirTimeoutEnvPipeline(env, -1, repo.Path, stdOut, stdErr); err != nil {
                return nil, fmt.Errorf("failed to run check-attr: %v\n%s\n%s", err, stdOut.String(), stdErr.String())
        }
 
index 27cb7fbebe191bf8b501541d9536f5f324db7382..38c01295b67cca0349acc314dbf9e07cb77844e4 100644 (file)
@@ -8,6 +8,7 @@ import (
        "bytes"
        "context"
        "os"
+       "path/filepath"
        "strings"
 
        "code.gitea.io/gitea/modules/log"
@@ -45,14 +46,15 @@ func (repo *Repository) readTreeToIndex(id SHA1, indexFilename ...string) error
 }
 
 // ReadTreeToTemporaryIndex reads a treeish to a temporary index file
-func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (filename string, cancel context.CancelFunc, err error) {
-       tmpIndex, err := os.CreateTemp("", "index")
+func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (filename, tmpDir string, cancel context.CancelFunc, err error) {
+       tmpDir, err = os.MkdirTemp("", "index")
        if err != nil {
                return
        }
-       filename = tmpIndex.Name()
+
+       filename = filepath.Join(tmpDir, ".tmp-index")
        cancel = func() {
-               err := util.Remove(filename)
+               err := util.RemoveAll(tmpDir)
                if err != nil {
                        log.Error("failed to remove tmp index file: %v", err)
                }
@@ -60,7 +62,7 @@ func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (filename strin
        err = repo.ReadTreeToIndex(treeish, filename)
        if err != nil {
                defer cancel()
-               return "", func() {}, err
+               return "", "", func() {}, err
        }
        return
 }
index d37827c3de6fc356173eeeeb91ab6d30a2f159da..037ec41ec6c7a141cccf613002c9fa353d67244d 100644 (file)
@@ -11,11 +11,10 @@ import (
        "bytes"
        "context"
        "io"
-       "os"
+       "strings"
 
        "code.gitea.io/gitea/modules/analyze"
        "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/util"
 
        "github.com/go-enry/go-enry/v2"
        "github.com/go-git/go-git/v5"
@@ -48,35 +47,28 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
        var checker *CheckAttributeReader
 
        if CheckGitVersionAtLeast("1.7.8") == nil {
-               indexFilename, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
+               indexFilename, workTree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
                if err == nil {
                        defer deleteTemporaryFile()
-                       tmpWorkTree, err := os.MkdirTemp("", "empty-work-dir")
-                       if err == nil {
-                               defer func() {
-                                       _ = util.RemoveAll(tmpWorkTree)
+                       checker = &CheckAttributeReader{
+                               Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
+                               Repo:       repo,
+                               IndexFile:  indexFilename,
+                               WorkTree:   workTree,
+                       }
+                       ctx, cancel := context.WithCancel(DefaultContext)
+                       if err := checker.Init(ctx); err != nil {
+                               log.Error("Unable to open checker for %s. Error: %v", commitID, err)
+                       } else {
+                               go func() {
+                                       err = checker.Run()
+                                       if err != nil {
+                                               log.Error("Unable to open checker for %s. Error: %v", commitID, err)
+                                               cancel()
+                                       }
                                }()
-
-                               checker = &CheckAttributeReader{
-                                       Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language"},
-                                       Repo:       repo,
-                                       IndexFile:  indexFilename,
-                                       WorkTree:   tmpWorkTree,
-                               }
-                               ctx, cancel := context.WithCancel(DefaultContext)
-                               if err := checker.Init(ctx); err != nil {
-                                       log.Error("Unable to open checker for %s. Error: %v", commitID, err)
-                               } else {
-                                       go func() {
-                                               err = checker.Run()
-                                               if err != nil {
-                                                       log.Error("Unable to open checker for %s. Error: %v", commitID, err)
-                                                       cancel()
-                                               }
-                                       }()
-                               }
-                               defer cancel()
                        }
+                       defer cancel()
                }
        }
 
@@ -114,6 +106,21 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
                                        sizes[language] += f.Size
 
                                        return nil
+                               } else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
+                                       // strip off a ? if present
+                                       if idx := strings.IndexByte(language, '?'); idx >= 0 {
+                                               language = language[:idx]
+                                       }
+                                       if len(language) != 0 {
+                                               // group languages, such as Pug -> HTML; SCSS -> CSS
+                                               group := enry.GetLanguageGroup(language)
+                                               if len(group) != 0 {
+                                                       language = group
+                                               }
+
+                                               sizes[language] += f.Size
+                                               return nil
+                                       }
                                }
                        }
                }
index 06269a466c72c0bb2f94d63d3affef38ca7adf97..4fda7ab6275e99f14026b8e793a0137899f72afa 100644 (file)
@@ -13,11 +13,10 @@ import (
        "context"
        "io"
        "math"
-       "os"
+       "strings"
 
        "code.gitea.io/gitea/modules/analyze"
        "code.gitea.io/gitea/modules/log"
-       "code.gitea.io/gitea/modules/util"
 
        "github.com/go-enry/go-enry/v2"
 )
@@ -68,35 +67,28 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
        var checker *CheckAttributeReader
 
        if CheckGitVersionAtLeast("1.7.8") == nil {
-               indexFilename, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
+               indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
                if err == nil {
                        defer deleteTemporaryFile()
-                       tmpWorkTree, err := os.MkdirTemp("", "empty-work-dir")
-                       if err == nil {
-                               defer func() {
-                                       _ = util.RemoveAll(tmpWorkTree)
+                       checker = &CheckAttributeReader{
+                               Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
+                               Repo:       repo,
+                               IndexFile:  indexFilename,
+                               WorkTree:   worktree,
+                       }
+                       ctx, cancel := context.WithCancel(DefaultContext)
+                       if err := checker.Init(ctx); err != nil {
+                               log.Error("Unable to open checker for %s. Error: %v", commitID, err)
+                       } else {
+                               go func() {
+                                       err = checker.Run()
+                                       if err != nil {
+                                               log.Error("Unable to open checker for %s. Error: %v", commitID, err)
+                                               cancel()
+                                       }
                                }()
-
-                               checker = &CheckAttributeReader{
-                                       Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language"},
-                                       Repo:       repo,
-                                       IndexFile:  indexFilename,
-                                       WorkTree:   tmpWorkTree,
-                               }
-                               ctx, cancel := context.WithCancel(DefaultContext)
-                               if err := checker.Init(ctx); err != nil {
-                                       log.Error("Unable to open checker for %s. Error: %v", commitID, err)
-                               } else {
-                                       go func() {
-                                               err = checker.Run()
-                                               if err != nil {
-                                                       log.Error("Unable to open checker for %s. Error: %v", commitID, err)
-                                                       cancel()
-                                               }
-                                       }()
-                               }
-                               defer cancel()
                        }
+                       defer cancel()
                }
        }
 
@@ -138,7 +130,23 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
 
                                        sizes[language] += f.Size()
                                        continue
+                               } else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
+                                       // strip off a ? if present
+                                       if idx := strings.IndexByte(language, '?'); idx >= 0 {
+                                               language = language[:idx]
+                                       }
+                                       if len(language) != 0 {
+                                               // group languages, such as Pug -> HTML; SCSS -> CSS
+                                               group := enry.GetLanguageGroup(language)
+                                               if len(group) != 0 {
+                                                       language = group
+                                               }
+
+                                               sizes[language] += f.Size()
+                                               continue
+                                       }
                                }
+
                        }
                }
 
index 9a876d2a6b21fa82e2494cba3016bdba43a47d71..04bd30bcebf5b8858fe210a1c890734111368e4a 100644 (file)
@@ -55,7 +55,7 @@ func NewContext() {
 }
 
 // Code returns a HTML version of code string with chroma syntax highlighting classes
-func Code(fileName, code string) string {
+func Code(fileName, language, code string) string {
        NewContext()
 
        // diff view newline will be passed as empty, change to literal \n so it can be copied
@@ -69,9 +69,23 @@ func Code(fileName, code string) string {
        }
 
        var lexer chroma.Lexer
-       if val, ok := highlightMapping[filepath.Ext(fileName)]; ok {
-               //use mapped value to find lexer
-               lexer = lexers.Get(val)
+
+       if len(language) > 0 {
+               lexer = lexers.Get(language)
+
+               if lexer == nil {
+                       // Attempt stripping off the '?'
+                       if idx := strings.IndexByte(language, '?'); idx > 0 {
+                               lexer = lexers.Get(language[:idx])
+                       }
+               }
+       }
+
+       if lexer == nil {
+               if val, ok := highlightMapping[filepath.Ext(fileName)]; ok {
+                       //use mapped value to find lexer
+                       lexer = lexers.Get(val)
+               }
        }
 
        if lexer == nil {
@@ -119,7 +133,7 @@ func CodeFromLexer(lexer chroma.Lexer, code string) string {
 }
 
 // File returns a slice of chroma syntax highlighted lines of code
-func File(numLines int, fileName string, code []byte) []string {
+func File(numLines int, fileName, language string, code []byte) []string {
        NewContext()
 
        if len(code) > sizeLimit {
@@ -139,8 +153,16 @@ func File(numLines int, fileName string, code []byte) []string {
        htmlw := bufio.NewWriter(&htmlbuf)
 
        var lexer chroma.Lexer
-       if val, ok := highlightMapping[filepath.Ext(fileName)]; ok {
-               lexer = lexers.Get(val)
+
+       // provided language overrides everything
+       if len(language) > 0 {
+               lexer = lexers.Get(language)
+       }
+
+       if lexer == nil {
+               if val, ok := highlightMapping[filepath.Ext(fileName)]; ok {
+                       lexer = lexers.Get(val)
+               }
        }
 
        if lexer == nil {
index 29a15c0b53b4446dfab20302e0ae3a66ba6d9334..3f47b6a48f65d488ace823d41cd72d80cb5e4b61 100644 (file)
@@ -96,7 +96,7 @@ steps:
 
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
-                       if got := File(tt.numLines, tt.fileName, []byte(tt.code)); !reflect.DeepEqual(got, tt.want) {
+                       if got := File(tt.numLines, tt.fileName, "", []byte(tt.code)); !reflect.DeepEqual(got, tt.want) {
                                t.Errorf("File() = %v, want %v", got, tt.want)
                        }
                })
index 51b7c9427d2d97b70a403e59280bf4729dbbb20f..bb8dcf16b3f494189a39b55c0f65c288f7c52260 100644 (file)
@@ -101,7 +101,7 @@ func searchResult(result *SearchResult, startIndex, endIndex int) (*Result, erro
                Language:       result.Language,
                Color:          result.Color,
                LineNumbers:    lineNumbers,
-               FormattedLines: highlight.Code(result.Filename, formattedLinesBuffer.String()),
+               FormattedLines: highlight.Code(result.Filename, "", formattedLinesBuffer.String()),
        }, nil
 }
 
index 463ce4ec6744f6f8101fcaca01e8891bb8170dc9..4bd1ef6f4dc950c4ff7deb58f628c4a8ef3f735f 100644 (file)
@@ -9,6 +9,7 @@ import (
 
        "code.gitea.io/gitea/models"
        "code.gitea.io/gitea/models/unittest"
+       "code.gitea.io/gitea/modules/json"
        "code.gitea.io/gitea/modules/test"
        "code.gitea.io/gitea/services/gitdiff"
 
@@ -118,13 +119,21 @@ func TestGetDiffPreview(t *testing.T) {
        t.Run("with given branch", func(t *testing.T) {
                diff, err := GetDiffPreview(ctx.Repo.Repository, branch, treePath, content)
                assert.NoError(t, err)
-               assert.EqualValues(t, expectedDiff, diff)
+               expectedBs, err := json.Marshal(expectedDiff)
+               assert.NoError(t, err)
+               bs, err := json.Marshal(diff)
+               assert.NoError(t, err)
+               assert.EqualValues(t, expectedBs, bs)
        })
 
        t.Run("empty branch, same results", func(t *testing.T) {
                diff, err := GetDiffPreview(ctx.Repo.Repository, "", treePath, content)
                assert.NoError(t, err)
-               assert.EqualValues(t, expectedDiff, diff)
+               expectedBs, err := json.Marshal(expectedDiff)
+               assert.NoError(t, err)
+               bs, err := json.Marshal(diff)
+               assert.NoError(t, err)
+               assert.EqualValues(t, expectedBs, bs)
        })
 }
 
index 110ec037e1dc3839f3ae31275201b5ac63f3a417..2fd72d81ab10aa0e48e1a24da635f4878853fb6c 100644 (file)
@@ -16,6 +16,7 @@ import (
        "code.gitea.io/gitea/modules/context"
        "code.gitea.io/gitea/modules/git"
        "code.gitea.io/gitea/modules/highlight"
+       "code.gitea.io/gitea/modules/log"
        "code.gitea.io/gitea/modules/templates"
        "code.gitea.io/gitea/modules/timeutil"
        "code.gitea.io/gitea/modules/util"
@@ -204,6 +205,31 @@ func processBlameParts(ctx *context.Context, blameParts []git.BlamePart) (map[st
 func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames map[string]*models.UserCommit, previousCommits map[string]string) {
        repoLink := ctx.Repo.RepoLink
 
+       language := ""
+
+       indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
+       if err == nil {
+               defer deleteTemporaryFile()
+
+               filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
+                       CachedOnly: true,
+                       Attributes: []string{"linguist-language", "gitlab-language"},
+                       Filenames:  []string{ctx.Repo.TreePath},
+                       IndexFile:  indexFilename,
+                       WorkTree:   worktree,
+               })
+               if err != nil {
+                       log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
+               }
+
+               language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
+               if language == "" || language == "unspecified" {
+                       language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
+               }
+               if language == "unspecified" {
+                       language = ""
+               }
+       }
        var lines = make([]string, 0)
        rows := make([]*blameRow, 0)
 
@@ -248,7 +274,7 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
                                line += "\n"
                        }
                        fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
-                       line = highlight.Code(fileName, line)
+                       line = highlight.Code(fileName, language, line)
 
                        br.Code = gotemplate.HTML(line)
                        rows = append(rows, br)
index 12b3aef505ea296b0b2511143a4ac5816a664c31..938292a37a6b0cc6237341437b29410b779bdf47 100644 (file)
@@ -502,7 +502,33 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
                        lineNums := linesBytesCount(buf)
                        ctx.Data["NumLines"] = strconv.Itoa(lineNums)
                        ctx.Data["NumLinesSet"] = true
-                       ctx.Data["FileContent"] = highlight.File(lineNums, blob.Name(), buf)
+
+                       language := ""
+
+                       indexFilename, worktree, deleteTemporaryFile, err := ctx.Repo.GitRepo.ReadTreeToTemporaryIndex(ctx.Repo.CommitID)
+                       if err == nil {
+                               defer deleteTemporaryFile()
+
+                               filename2attribute2info, err := ctx.Repo.GitRepo.CheckAttribute(git.CheckAttributeOpts{
+                                       CachedOnly: true,
+                                       Attributes: []string{"linguist-language", "gitlab-language"},
+                                       Filenames:  []string{ctx.Repo.TreePath},
+                                       IndexFile:  indexFilename,
+                                       WorkTree:   worktree,
+                               })
+                               if err != nil {
+                                       log.Error("Unable to load attributes for %-v:%s. Error: %v", ctx.Repo.Repository, ctx.Repo.TreePath, err)
+                               }
+
+                               language = filename2attribute2info[ctx.Repo.TreePath]["linguist-language"]
+                               if language == "" || language == "unspecified" {
+                                       language = filename2attribute2info[ctx.Repo.TreePath]["gitlab-language"]
+                               }
+                               if language == "unspecified" {
+                                       language = ""
+                               }
+                       }
+                       ctx.Data["FileContent"] = highlight.File(lineNums, blob.Name(), language, buf)
                }
                if !isLFSFile {
                        if ctx.Repo.CanEnableEditor() {
index 614f8104ecaf2d9c2ac5b14fa482e7b5bd0d391c..33e66e89ec131fd0e5ada7a1b53e5aa7a737fb48 100644 (file)
@@ -31,7 +31,6 @@ import (
        "code.gitea.io/gitea/modules/log"
        "code.gitea.io/gitea/modules/process"
        "code.gitea.io/gitea/modules/setting"
-       "code.gitea.io/gitea/modules/util"
 
        "github.com/sergi/go-diff/diffmatchpatch"
        stdcharset "golang.org/x/net/html/charset"
@@ -178,6 +177,7 @@ func getLineContent(content string) string {
 
 // DiffSection represents a section of a DiffFile.
 type DiffSection struct {
+       file     *DiffFile
        FileName string
        Name     string
        Lines    []*DiffLine
@@ -546,6 +546,11 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) tem
                diff2           string
        )
 
+       language := ""
+       if diffSection.file != nil {
+               language = diffSection.file.Language
+       }
+
        // try to find equivalent diff line. ignore, otherwise
        switch diffLine.Type {
        case DiffLineSection:
@@ -553,25 +558,25 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) tem
        case DiffLineAdd:
                compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
                if compareDiffLine == nil {
-                       return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]))
+                       return template.HTML(highlight.Code(diffSection.FileName, language, diffLine.Content[1:]))
                }
                diff1 = compareDiffLine.Content
                diff2 = diffLine.Content
        case DiffLineDel:
                compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
                if compareDiffLine == nil {
-                       return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]))
+                       return template.HTML(highlight.Code(diffSection.FileName, language, diffLine.Content[1:]))
                }
                diff1 = diffLine.Content
                diff2 = compareDiffLine.Content
        default:
                if strings.IndexByte(" +-", diffLine.Content[0]) > -1 {
-                       return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]))
+                       return template.HTML(highlight.Code(diffSection.FileName, language, diffLine.Content[1:]))
                }
-               return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content))
+               return template.HTML(highlight.Code(diffSection.FileName, language, diffLine.Content))
        }
 
-       diffRecord := diffMatchPatch.DiffMain(highlight.Code(diffSection.FileName, diff1[1:]), highlight.Code(diffSection.FileName, diff2[1:]), true)
+       diffRecord := diffMatchPatch.DiffMain(highlight.Code(diffSection.FileName, language, diff1[1:]), highlight.Code(diffSection.FileName, language, diff2[1:]), true)
        diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
 
        return diffToHTML(diffSection.FileName, diffRecord, diffLine.Type)
@@ -597,6 +602,7 @@ type DiffFile struct {
        IsProtected             bool
        IsGenerated             bool
        IsVendored              bool
+       Language                string
 }
 
 // GetType returns type of diff file.
@@ -1008,7 +1014,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
                        line := sb.String()
 
                        // Create a new section to represent this hunk
-                       curSection = &DiffSection{}
+                       curSection = &DiffSection{file: curFile}
                        lastLeftIdx = -1
                        curFile.Sections = append(curFile.Sections, curSection)
 
@@ -1048,7 +1054,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
                        rightLine++
                        if curSection == nil {
                                // Create a new section to represent this hunk
-                               curSection = &DiffSection{}
+                               curSection = &DiffSection{file: curFile}
                                curFile.Sections = append(curFile.Sections, curSection)
                                lastLeftIdx = -1
                        }
@@ -1074,7 +1080,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
                        }
                        if curSection == nil {
                                // Create a new section to represent this hunk
-                               curSection = &DiffSection{}
+                               curSection = &DiffSection{file: curFile}
                                curFile.Sections = append(curFile.Sections, curSection)
                                lastLeftIdx = -1
                        }
@@ -1094,7 +1100,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
                        lastLeftIdx = -1
                        if curSection == nil {
                                // Create a new section to represent this hunk
-                               curSection = &DiffSection{}
+                               curSection = &DiffSection{file: curFile}
                                curFile.Sections = append(curFile.Sections, curSection)
                        }
                        curSection.Lines = append(curSection.Lines, diffLine)
@@ -1302,23 +1308,15 @@ func GetDiffRangeWithWhitespaceBehavior(gitRepo *git.Repository, beforeCommitID,
        var checker *git.CheckAttributeReader
 
        if git.CheckGitVersionAtLeast("1.7.8") == nil {
-               indexFilename, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(afterCommitID)
+               indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(afterCommitID)
                if err == nil {
                        defer deleteTemporaryFile()
-                       workdir, err := os.MkdirTemp("", "empty-work-dir")
-                       if err != nil {
-                               log.Error("Unable to create temporary directory: %v", err)
-                               return nil, err
-                       }
-                       defer func() {
-                               _ = util.RemoveAll(workdir)
-                       }()
 
                        checker = &git.CheckAttributeReader{
-                               Attributes: []string{"linguist-vendored", "linguist-generated"},
+                               Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
                                Repo:       gitRepo,
                                IndexFile:  indexFilename,
-                               WorkTree:   workdir,
+                               WorkTree:   worktree,
                        }
                        ctx, cancel := context.WithCancel(git.DefaultContext)
                        if err := checker.Init(ctx); err != nil {
@@ -1361,6 +1359,11 @@ func GetDiffRangeWithWhitespaceBehavior(gitRepo *git.Repository, beforeCommitID,
                                                gotGenerated = generated == "false"
                                        }
                                }
+                               if language, has := attrs["linguist-language"]; has && language != "unspecified" && language != "" {
+                                       diffFile.Language = language
+                               } else if language, has := attrs["gitlab-language"]; has && language != "unspecified" && language != "" {
+                                       diffFile.Language = language
+                               }
                        } else {
                                log.Error("Unexpected error: %v", err)
                        }
index aefd396ebbae83c3c1bf44264e5ee8a26b7e0544..c6c6f3b0e358bc572ee1f41e1661865dfd472102 100644 (file)
@@ -533,7 +533,7 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
 
 func TestDiffToHTML_14231(t *testing.T) {
        setting.Cfg = ini.Empty()
-       diffRecord := diffMatchPatch.DiffMain(highlight.Code("main.v", "                run()\n"), highlight.Code("main.v", "           run(db)\n"), true)
+       diffRecord := diffMatchPatch.DiffMain(highlight.Code("main.v", "", "            run()\n"), highlight.Code("main.v", "", "               run(db)\n"), true)
        diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
 
        expected := `           <span class="n">run</span><span class="added-code"><span class="o">(</span><span class="n">db</span></span><span class="o">)</span>`