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 <art27@cantab.net>
This commit is contained in:
parent
81a4fc7528
commit
3c4724d70e
13 changed files with 223 additions and 97 deletions
|
@ -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]`.
|
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.
|
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`)
|
## Time (`time`)
|
||||||
|
|
||||||
- `FORMAT`: Time format to display on UI. i.e. RFC1123 or 2006-01-02 15:04:05
|
- `FORMAT`: Time format to display on UI. i.e. RFC1123 or 2006-01-02 15:04:05
|
||||||
|
|
|
@ -22,6 +22,8 @@ type CheckAttributeOpts struct {
|
||||||
AllAttributes bool
|
AllAttributes bool
|
||||||
Attributes []string
|
Attributes []string
|
||||||
Filenames []string
|
Filenames []string
|
||||||
|
IndexFile string
|
||||||
|
WorkTree string
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckAttribute return the Blame object of file
|
// 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)
|
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)
|
stdOut := new(bytes.Buffer)
|
||||||
stdErr := new(bytes.Buffer)
|
stdErr := new(bytes.Buffer)
|
||||||
|
|
||||||
|
@ -61,7 +76,7 @@ func (repo *Repository) CheckAttribute(opts CheckAttributeOpts) (map[string]map[
|
||||||
|
|
||||||
cmd := NewCommand(cmdArgs...)
|
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())
|
return nil, fmt.Errorf("failed to run check-attr: %v\n%s\n%s", err, stdOut.String(), stdErr.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"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
|
// ReadTreeToTemporaryIndex reads a treeish to a temporary index file
|
||||||
func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (filename string, cancel context.CancelFunc, err error) {
|
func (repo *Repository) ReadTreeToTemporaryIndex(treeish string) (filename, tmpDir string, cancel context.CancelFunc, err error) {
|
||||||
tmpIndex, err := os.CreateTemp("", "index")
|
tmpDir, err = os.MkdirTemp("", "index")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
filename = tmpIndex.Name()
|
|
||||||
|
filename = filepath.Join(tmpDir, ".tmp-index")
|
||||||
cancel = func() {
|
cancel = func() {
|
||||||
err := util.Remove(filename)
|
err := util.RemoveAll(tmpDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("failed to remove tmp index file: %v", err)
|
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)
|
err = repo.ReadTreeToIndex(treeish, filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return "", func() {}, err
|
return "", "", func() {}, err
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,10 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/analyze"
|
"code.gitea.io/gitea/modules/analyze"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
|
|
||||||
"github.com/go-enry/go-enry/v2"
|
"github.com/go-enry/go-enry/v2"
|
||||||
"github.com/go-git/go-git/v5"
|
"github.com/go-git/go-git/v5"
|
||||||
|
@ -48,35 +47,28 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
||||||
var checker *CheckAttributeReader
|
var checker *CheckAttributeReader
|
||||||
|
|
||||||
if CheckGitVersionAtLeast("1.7.8") == nil {
|
if CheckGitVersionAtLeast("1.7.8") == nil {
|
||||||
indexFilename, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
|
indexFilename, workTree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer deleteTemporaryFile()
|
defer deleteTemporaryFile()
|
||||||
tmpWorkTree, err := os.MkdirTemp("", "empty-work-dir")
|
checker = &CheckAttributeReader{
|
||||||
if err == nil {
|
Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
|
||||||
defer func() {
|
Repo: repo,
|
||||||
_ = util.RemoveAll(tmpWorkTree)
|
IndexFile: indexFilename,
|
||||||
}()
|
WorkTree: workTree,
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
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
|
sizes[language] += f.Size
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/analyze"
|
"code.gitea.io/gitea/modules/analyze"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
|
|
||||||
"github.com/go-enry/go-enry/v2"
|
"github.com/go-enry/go-enry/v2"
|
||||||
)
|
)
|
||||||
|
@ -68,35 +67,28 @@ func (repo *Repository) GetLanguageStats(commitID string) (map[string]int64, err
|
||||||
var checker *CheckAttributeReader
|
var checker *CheckAttributeReader
|
||||||
|
|
||||||
if CheckGitVersionAtLeast("1.7.8") == nil {
|
if CheckGitVersionAtLeast("1.7.8") == nil {
|
||||||
indexFilename, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
|
indexFilename, worktree, deleteTemporaryFile, err := repo.ReadTreeToTemporaryIndex(commitID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer deleteTemporaryFile()
|
defer deleteTemporaryFile()
|
||||||
tmpWorkTree, err := os.MkdirTemp("", "empty-work-dir")
|
checker = &CheckAttributeReader{
|
||||||
if err == nil {
|
Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
|
||||||
defer func() {
|
Repo: repo,
|
||||||
_ = util.RemoveAll(tmpWorkTree)
|
IndexFile: indexFilename,
|
||||||
}()
|
WorkTree: worktree,
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
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()
|
sizes[language] += f.Size()
|
||||||
continue
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ func NewContext() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code returns a HTML version of code string with chroma syntax highlighting classes
|
// 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()
|
NewContext()
|
||||||
|
|
||||||
// diff view newline will be passed as empty, change to literal \n so it can be copied
|
// 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
|
var lexer chroma.Lexer
|
||||||
if val, ok := highlightMapping[filepath.Ext(fileName)]; ok {
|
|
||||||
//use mapped value to find lexer
|
if len(language) > 0 {
|
||||||
lexer = lexers.Get(val)
|
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 {
|
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
|
// 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()
|
NewContext()
|
||||||
|
|
||||||
if len(code) > sizeLimit {
|
if len(code) > sizeLimit {
|
||||||
|
@ -139,8 +153,16 @@ func File(numLines int, fileName string, code []byte) []string {
|
||||||
htmlw := bufio.NewWriter(&htmlbuf)
|
htmlw := bufio.NewWriter(&htmlbuf)
|
||||||
|
|
||||||
var lexer chroma.Lexer
|
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 {
|
if lexer == nil {
|
||||||
|
|
|
@ -96,7 +96,7 @@ steps:
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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)
|
t.Errorf("File() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -101,7 +101,7 @@ func searchResult(result *SearchResult, startIndex, endIndex int) (*Result, erro
|
||||||
Language: result.Language,
|
Language: result.Language,
|
||||||
Color: result.Color,
|
Color: result.Color,
|
||||||
LineNumbers: lineNumbers,
|
LineNumbers: lineNumbers,
|
||||||
FormattedLines: highlight.Code(result.Filename, formattedLinesBuffer.String()),
|
FormattedLines: highlight.Code(result.Filename, "", formattedLinesBuffer.String()),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/gitea/services/gitdiff"
|
"code.gitea.io/gitea/services/gitdiff"
|
||||||
|
|
||||||
|
@ -118,13 +119,21 @@ func TestGetDiffPreview(t *testing.T) {
|
||||||
t.Run("with given branch", func(t *testing.T) {
|
t.Run("with given branch", func(t *testing.T) {
|
||||||
diff, err := GetDiffPreview(ctx.Repo.Repository, branch, treePath, content)
|
diff, err := GetDiffPreview(ctx.Repo.Repository, branch, treePath, content)
|
||||||
assert.NoError(t, err)
|
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) {
|
t.Run("empty branch, same results", func(t *testing.T) {
|
||||||
diff, err := GetDiffPreview(ctx.Repo.Repository, "", treePath, content)
|
diff, err := GetDiffPreview(ctx.Repo.Repository, "", treePath, content)
|
||||||
assert.NoError(t, err)
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/highlight"
|
"code.gitea.io/gitea/modules/highlight"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"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) {
|
func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames map[string]*models.UserCommit, previousCommits map[string]string) {
|
||||||
repoLink := ctx.Repo.RepoLink
|
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)
|
var lines = make([]string, 0)
|
||||||
rows := make([]*blameRow, 0)
|
rows := make([]*blameRow, 0)
|
||||||
|
|
||||||
|
@ -248,7 +274,7 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
|
||||||
line += "\n"
|
line += "\n"
|
||||||
}
|
}
|
||||||
fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
|
fileName := fmt.Sprintf("%v", ctx.Data["FileName"])
|
||||||
line = highlight.Code(fileName, line)
|
line = highlight.Code(fileName, language, line)
|
||||||
|
|
||||||
br.Code = gotemplate.HTML(line)
|
br.Code = gotemplate.HTML(line)
|
||||||
rows = append(rows, br)
|
rows = append(rows, br)
|
||||||
|
|
|
@ -502,7 +502,33 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
|
||||||
lineNums := linesBytesCount(buf)
|
lineNums := linesBytesCount(buf)
|
||||||
ctx.Data["NumLines"] = strconv.Itoa(lineNums)
|
ctx.Data["NumLines"] = strconv.Itoa(lineNums)
|
||||||
ctx.Data["NumLinesSet"] = true
|
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 !isLFSFile {
|
||||||
if ctx.Repo.CanEnableEditor() {
|
if ctx.Repo.CanEnableEditor() {
|
||||||
|
|
|
@ -31,7 +31,6 @@ import (
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/process"
|
"code.gitea.io/gitea/modules/process"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
|
||||||
|
|
||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
"github.com/sergi/go-diff/diffmatchpatch"
|
||||||
stdcharset "golang.org/x/net/html/charset"
|
stdcharset "golang.org/x/net/html/charset"
|
||||||
|
@ -178,6 +177,7 @@ func getLineContent(content string) string {
|
||||||
|
|
||||||
// DiffSection represents a section of a DiffFile.
|
// DiffSection represents a section of a DiffFile.
|
||||||
type DiffSection struct {
|
type DiffSection struct {
|
||||||
|
file *DiffFile
|
||||||
FileName string
|
FileName string
|
||||||
Name string
|
Name string
|
||||||
Lines []*DiffLine
|
Lines []*DiffLine
|
||||||
|
@ -546,6 +546,11 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) tem
|
||||||
diff2 string
|
diff2 string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
language := ""
|
||||||
|
if diffSection.file != nil {
|
||||||
|
language = diffSection.file.Language
|
||||||
|
}
|
||||||
|
|
||||||
// try to find equivalent diff line. ignore, otherwise
|
// try to find equivalent diff line. ignore, otherwise
|
||||||
switch diffLine.Type {
|
switch diffLine.Type {
|
||||||
case DiffLineSection:
|
case DiffLineSection:
|
||||||
|
@ -553,25 +558,25 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) tem
|
||||||
case DiffLineAdd:
|
case DiffLineAdd:
|
||||||
compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
|
compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
|
||||||
if compareDiffLine == nil {
|
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
|
diff1 = compareDiffLine.Content
|
||||||
diff2 = diffLine.Content
|
diff2 = diffLine.Content
|
||||||
case DiffLineDel:
|
case DiffLineDel:
|
||||||
compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
|
compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
|
||||||
if compareDiffLine == nil {
|
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
|
diff1 = diffLine.Content
|
||||||
diff2 = compareDiffLine.Content
|
diff2 = compareDiffLine.Content
|
||||||
default:
|
default:
|
||||||
if strings.IndexByte(" +-", diffLine.Content[0]) > -1 {
|
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)
|
diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
|
||||||
|
|
||||||
return diffToHTML(diffSection.FileName, diffRecord, diffLine.Type)
|
return diffToHTML(diffSection.FileName, diffRecord, diffLine.Type)
|
||||||
|
@ -597,6 +602,7 @@ type DiffFile struct {
|
||||||
IsProtected bool
|
IsProtected bool
|
||||||
IsGenerated bool
|
IsGenerated bool
|
||||||
IsVendored bool
|
IsVendored bool
|
||||||
|
Language string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetType returns type of diff file.
|
// GetType returns type of diff file.
|
||||||
|
@ -1008,7 +1014,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
|
||||||
line := sb.String()
|
line := sb.String()
|
||||||
|
|
||||||
// Create a new section to represent this hunk
|
// Create a new section to represent this hunk
|
||||||
curSection = &DiffSection{}
|
curSection = &DiffSection{file: curFile}
|
||||||
lastLeftIdx = -1
|
lastLeftIdx = -1
|
||||||
curFile.Sections = append(curFile.Sections, curSection)
|
curFile.Sections = append(curFile.Sections, curSection)
|
||||||
|
|
||||||
|
@ -1048,7 +1054,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
|
||||||
rightLine++
|
rightLine++
|
||||||
if curSection == nil {
|
if curSection == nil {
|
||||||
// Create a new section to represent this hunk
|
// Create a new section to represent this hunk
|
||||||
curSection = &DiffSection{}
|
curSection = &DiffSection{file: curFile}
|
||||||
curFile.Sections = append(curFile.Sections, curSection)
|
curFile.Sections = append(curFile.Sections, curSection)
|
||||||
lastLeftIdx = -1
|
lastLeftIdx = -1
|
||||||
}
|
}
|
||||||
|
@ -1074,7 +1080,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
|
||||||
}
|
}
|
||||||
if curSection == nil {
|
if curSection == nil {
|
||||||
// Create a new section to represent this hunk
|
// Create a new section to represent this hunk
|
||||||
curSection = &DiffSection{}
|
curSection = &DiffSection{file: curFile}
|
||||||
curFile.Sections = append(curFile.Sections, curSection)
|
curFile.Sections = append(curFile.Sections, curSection)
|
||||||
lastLeftIdx = -1
|
lastLeftIdx = -1
|
||||||
}
|
}
|
||||||
|
@ -1094,7 +1100,7 @@ func parseHunks(curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio
|
||||||
lastLeftIdx = -1
|
lastLeftIdx = -1
|
||||||
if curSection == nil {
|
if curSection == nil {
|
||||||
// Create a new section to represent this hunk
|
// Create a new section to represent this hunk
|
||||||
curSection = &DiffSection{}
|
curSection = &DiffSection{file: curFile}
|
||||||
curFile.Sections = append(curFile.Sections, curSection)
|
curFile.Sections = append(curFile.Sections, curSection)
|
||||||
}
|
}
|
||||||
curSection.Lines = append(curSection.Lines, diffLine)
|
curSection.Lines = append(curSection.Lines, diffLine)
|
||||||
|
@ -1302,23 +1308,15 @@ func GetDiffRangeWithWhitespaceBehavior(gitRepo *git.Repository, beforeCommitID,
|
||||||
var checker *git.CheckAttributeReader
|
var checker *git.CheckAttributeReader
|
||||||
|
|
||||||
if git.CheckGitVersionAtLeast("1.7.8") == nil {
|
if git.CheckGitVersionAtLeast("1.7.8") == nil {
|
||||||
indexFilename, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(afterCommitID)
|
indexFilename, worktree, deleteTemporaryFile, err := gitRepo.ReadTreeToTemporaryIndex(afterCommitID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer deleteTemporaryFile()
|
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{
|
checker = &git.CheckAttributeReader{
|
||||||
Attributes: []string{"linguist-vendored", "linguist-generated"},
|
Attributes: []string{"linguist-vendored", "linguist-generated", "linguist-language", "gitlab-language"},
|
||||||
Repo: gitRepo,
|
Repo: gitRepo,
|
||||||
IndexFile: indexFilename,
|
IndexFile: indexFilename,
|
||||||
WorkTree: workdir,
|
WorkTree: worktree,
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(git.DefaultContext)
|
ctx, cancel := context.WithCancel(git.DefaultContext)
|
||||||
if err := checker.Init(ctx); err != nil {
|
if err := checker.Init(ctx); err != nil {
|
||||||
|
@ -1361,6 +1359,11 @@ func GetDiffRangeWithWhitespaceBehavior(gitRepo *git.Repository, beforeCommitID,
|
||||||
gotGenerated = generated == "false"
|
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 {
|
} else {
|
||||||
log.Error("Unexpected error: %v", err)
|
log.Error("Unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -533,7 +533,7 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
|
||||||
|
|
||||||
func TestDiffToHTML_14231(t *testing.T) {
|
func TestDiffToHTML_14231(t *testing.T) {
|
||||||
setting.Cfg = ini.Empty()
|
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)
|
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>`
|
expected := ` <span class="n">run</span><span class="added-code"><span class="o">(</span><span class="n">db</span></span><span class="o">)</span>`
|
||||||
|
|
Loading…
Add table
Reference in a new issue