From 3c4724d70e4ac7bfc06b97f6fad8936f97479b6b Mon Sep 17 00:00:00 2001 From: zeripath Date: Wed, 17 Nov 2021 20:37:00 +0000 Subject: [PATCH] Add .gitattribute assisted language detection to blame, diff and render (#17590) 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 --- .../doc/advanced/config-cheat-sheet.en-us.md | 8 +++ modules/git/repo_attribute.go | 17 ++++- modules/git/repo_index.go | 12 ++-- modules/git/repo_language_stats_gogit.go | 63 ++++++++++-------- modules/git/repo_language_stats_nogogit.go | 64 +++++++++++-------- modules/highlight/highlight.go | 36 +++++++++-- modules/highlight/highlight_test.go | 2 +- modules/indexer/code/search.go | 2 +- modules/repofiles/diff_test.go | 13 +++- routers/web/repo/blame.go | 28 +++++++- routers/web/repo/view.go | 28 +++++++- services/gitdiff/gitdiff.go | 45 +++++++------ services/gitdiff/gitdiff_test.go | 2 +- 13 files changed, 223 insertions(+), 97 deletions(-) diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 3b5d9213d..a087b253e 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -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 diff --git a/modules/git/repo_attribute.go b/modules/git/repo_attribute.go index aace64425..88fb7810a 100644 --- a/modules/git/repo_attribute.go +++ b/modules/git/repo_attribute.go @@ -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()) } diff --git a/modules/git/repo_index.go b/modules/git/repo_index.go index 27cb7fbeb..38c01295b 100644 --- a/modules/git/repo_index.go +++ b/modules/git/repo_index.go @@ -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 } diff --git a/modules/git/repo_language_stats_gogit.go b/modules/git/repo_language_stats_gogit.go index d37827c3d..037ec41ec 100644 --- a/modules/git/repo_language_stats_gogit.go +++ b/modules/git/repo_language_stats_gogit.go @@ -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"}, - 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() + 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() + } + }() + } + 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 + } } } } diff --git a/modules/git/repo_language_stats_nogogit.go b/modules/git/repo_language_stats_nogogit.go index 06269a466..4fda7ab62 100644 --- a/modules/git/repo_language_stats_nogogit.go +++ b/modules/git/repo_language_stats_nogogit.go @@ -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"}, - 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() + 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() + } + }() + } + 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 + } } + } } diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go index 9a876d2a6..04bd30bce 100644 --- a/modules/highlight/highlight.go +++ b/modules/highlight/highlight.go @@ -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 { diff --git a/modules/highlight/highlight_test.go b/modules/highlight/highlight_test.go index 29a15c0b5..3f47b6a48 100644 --- a/modules/highlight/highlight_test.go +++ b/modules/highlight/highlight_test.go @@ -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) } }) diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index 51b7c9427..bb8dcf16b 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -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 } diff --git a/modules/repofiles/diff_test.go b/modules/repofiles/diff_test.go index 463ce4ec6..4bd1ef6f4 100644 --- a/modules/repofiles/diff_test.go +++ b/modules/repofiles/diff_test.go @@ -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) }) } diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go index 110ec037e..2fd72d81a 100644 --- a/routers/web/repo/blame.go +++ b/routers/web/repo/blame.go @@ -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) diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go index 12b3aef50..938292a37 100644 --- a/routers/web/repo/view.go +++ b/routers/web/repo/view.go @@ -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() { diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 614f8104e..33e66e89e 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -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) } diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index aefd396eb..c6c6f3b0e 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -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 := ` run(db)`