diff --git a/modules/git/attributes.go b/modules/git/attributes.go new file mode 100644 index 0000000000000..b675c0a10e94d --- /dev/null +++ b/modules/git/attributes.go @@ -0,0 +1,100 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package git + +import ( + "bufio" + "io" + "path" + "regexp" + "strings" + + "github.com/gobwas/glob" +) + +type attributePattern struct { + pattern glob.Glob + attributes map[string]interface{} +} + +// Attributes represents all attributes from a .gitattribute file +type Attributes []attributePattern + +// ForFile returns the git attributes for the given path. +func (a Attributes) ForFile(filepath string) map[string]interface{} { + filepath = path.Join("/", filepath) + + for _, pattern := range a { + if pattern.pattern.Match(filepath) { + return pattern.attributes + } + } + + return map[string]interface{}{} +} + +var whitespaceSplit = regexp.MustCompile(`\s+`) + +// ParseAttributes parses git attributes from the provided reader. +func ParseAttributes(reader io.Reader) (Attributes, error) { + patterns := []attributePattern{} + + scanner := bufio.NewScanner(reader) + for scanner.Scan() { + line := scanner.Text() + + // Skip empty lines and comments + if len(line) == 0 || line[0] == '#' { + continue + } + + splitted := whitespaceSplit.Split(line, 2) + + pattern := path.Join("/", splitted[0]) + + attributes := map[string]interface{}{} + if len(splitted) == 2 { + attributes = parseAttributes(splitted[1]) + } + + if g, err := glob.Compile(pattern, '/'); err == nil { + patterns = append(patterns, attributePattern{ + g, + attributes, + }) + } + } + + for i, j := 0, len(patterns)-1; i < j; i, j = i+1, j-1 { + patterns[i], patterns[j] = patterns[j], patterns[i] + } + + return Attributes(patterns), scanner.Err() +} + +// parseAttributes parses an attribute string. Attributes can have the following formats: +// foo => foo = true +// -foo => foo = false +// foo=bar => foo = bar +func parseAttributes(attributes string) map[string]interface{} { + values := make(map[string]interface{}) + + for _, chunk := range whitespaceSplit.Split(attributes, -1) { + if chunk == "=" { // "foo = bar" is treated as "foo" and "bar" + continue + } + + if strings.HasPrefix(chunk, "-") { // "-foo" + values[chunk[1:]] = false + } else if strings.Contains(chunk, "=") { // "foo=bar" + splitted := strings.SplitN(chunk, "=", 2) + values[splitted[0]] = splitted[1] + } else { // "foo" + values[chunk] = true + } + } + + return values +} diff --git a/modules/git/attributes_test.go b/modules/git/attributes_test.go new file mode 100644 index 0000000000000..4c3f656308775 --- /dev/null +++ b/modules/git/attributes_test.go @@ -0,0 +1,116 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package git + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseAttributes(t *testing.T) { + attributes, err := ParseAttributes(strings.NewReader("* foo -bar foo2 = bar2 foobar=test")) + assert.NoError(t, err) + assert.Len(t, attributes, 1) + assert.Len(t, attributes[0].attributes, 5) + assert.Contains(t, attributes[0].attributes, "foo") + assert.True(t, attributes[0].attributes["foo"].(bool)) + assert.Contains(t, attributes[0].attributes, "bar") + assert.False(t, attributes[0].attributes["bar"].(bool)) + assert.Contains(t, attributes[0].attributes, "foo2") + assert.True(t, attributes[0].attributes["foo2"].(bool)) + assert.Contains(t, attributes[0].attributes, "bar2") + assert.True(t, attributes[0].attributes["bar2"].(bool)) + assert.Contains(t, attributes[0].attributes, "foobar") + assert.Equal(t, "test", attributes[0].attributes["foobar"].(string)) +} + +func TestForFile(t *testing.T) { + input := `* text=auto eol=lf +/vendor/** -text -eol linguist-vendored +/public/vendor/** -text -eol linguist-vendored +/templates/**/*.tmpl linguist-language=Handlebars +/.eslintrc linguist-language=YAML +/.stylelintrc linguist-language=YAML` + + attributes, err := ParseAttributes(strings.NewReader(input)) + assert.NoError(t, err) + assert.Len(t, attributes, 6) + + cases := []struct { + filepath string + expectedKey string + expectedValue interface{} + }{ + // case 0 + { + "test.txt", + "text", + "auto", + }, + // case 1 + { + "test.txt", + "eol", + "lf", + }, + // case 2 + { + "/vendor/test.txt", + "text", + false, + }, + // case 3 + { + "/vendor/test.txt", + "eol", + false, + }, + // case 4 + { + "vendor/test.txt", + "linguist-vendored", + true, + }, + // case 5 + { + "/vendor/dir/dir/dir/test.txt", + "linguist-vendored", + true, + }, + // case 6 + { + ".eslintrc", + "linguist-language", + "YAML", + }, + // case 7 + { + "/.eslintrc", + "linguist-language", + "YAML", + }, + } + + for n, c := range cases { + fa := attributes.ForFile(c.filepath) + assert.Contains(t, fa, c.expectedKey, "case %d", n) + assert.Equal(t, c.expectedValue, fa[c.expectedKey], "case %d", n) + } +} + +func TestForFileSecondWins(t *testing.T) { + input := `*.txt foo +*.txt bar` + + attributes, err := ParseAttributes(strings.NewReader(input)) + assert.NoError(t, err) + assert.Len(t, attributes, 2) + + fa := attributes.ForFile("test.txt") + assert.Contains(t, fa, "bar") + assert.NotContains(t, fa, "foo") +} diff --git a/modules/git/repo.go b/modules/git/repo.go index 43f329f4487d6..881e8bf7c0eb0 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -12,6 +12,7 @@ import ( "fmt" "os" "path" + "path/filepath" "strconv" "strings" "time" @@ -396,3 +397,19 @@ func GetDivergingCommits(repoPath string, baseBranch string, targetBranch string return DivergeObject{ahead, behind}, nil } + +// GetGitAttributes returns the parsed git attributes from repo.git/info/attributes or nil if not present. +func (repo *Repository) GetGitAttributes() (Attributes, error) { + attributesPath := filepath.Join(repo.Path, "info", "attributes") + + attributesFile, err := os.Open(attributesPath) + if err != nil { + if os.IsNotExist(err) { + return Attributes{}, nil + } + return nil, err + } + defer attributesFile.Close() + + return ParseAttributes(attributesFile) +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 59ee6e48ea7f8..bfed3f2a69770 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1956,6 +1956,7 @@ diff.stats_desc = %d changed files with %d additions

@@ -111,6 +112,7 @@
{{if $showFileViewToggle}}
+ {{if $isRenderable}} {{if $isImage}} {{template "repo/diff/image_diff" dict "file" . "root" $ "blobBase" $blobBase "blobHead" $blobHead}} @@ -118,6 +120,11 @@ {{template "repo/diff/csv_diff" dict "file" . "root" $}} {{end}}
+ {{else}} +
+ {{$.i18n.Tr "repo.diff.generated_not_shown"}} +
+ {{end}}
{{end}}