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
diff.stats_desc_file = %d changes: %d additions and %d deletions
diff.bin = BIN
diff.bin_not_shown = Binary file not shown.
+diff.generated_not_shown = Generated file not shown.
diff.view_file = View File
diff.file_before = Before
diff.file_after = After
diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go
index f8f0fd7e3b90b..7bcb24ad786cc 100644
--- a/services/gitdiff/gitdiff.go
+++ b/services/gitdiff/gitdiff.go
@@ -590,6 +590,7 @@ type DiffFile struct {
IsIncomplete bool
IsIncompleteLineTooLong bool
IsProtected bool
+ IsGenerated bool
}
// GetType returns type of diff file.
@@ -1248,7 +1249,21 @@ func GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID
if err != nil {
return nil, fmt.Errorf("ParsePatch: %v", err)
}
+
+ gitAttributes, err := gitRepo.GetGitAttributes()
+ if err != nil {
+ log.Error("GetGitAttributes: %v", err)
+ }
+
for _, diffFile := range diff.Files {
+ if generated, ok := gitAttributes.ForFile(diffFile.Name)["linguist-generated"]; ok {
+ if b, ok := generated.(bool); ok && b {
+ diffFile.IsGenerated = true
+ } else if s, ok := generated.(string); ok && s == "true" {
+ diffFile.IsGenerated = true
+ }
+ }
+
tailSection := diffFile.GetTailSection(gitRepo, beforeCommitID, afterCommitID)
if tailSection != nil {
diffFile.Sections = append(diffFile.Sections, tailSection)
diff --git a/services/repository/attributes.go b/services/repository/attributes.go
new file mode 100644
index 0000000000000..3c7f293a9db50
--- /dev/null
+++ b/services/repository/attributes.go
@@ -0,0 +1,51 @@
+// 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 repository
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ "code.gitea.io/gitea/modules/git"
+)
+
+// SyncGitAttributes copies the content of the .gitattributes file from the default branch into repo.git/info/attributes.
+func SyncGitAttributes(gitRepo *git.Repository, sourceBranch string) error {
+ commit, err := gitRepo.GetBranchCommit(sourceBranch)
+ if err != nil {
+ return err
+ }
+
+ attributesBlob, err := commit.GetBlobByPath("/.gitattributes")
+ if err != nil {
+ if git.IsErrNotExist(err) {
+ return nil
+ }
+ return err
+ }
+
+ infoPath := filepath.Join(gitRepo.Path, "info")
+ if err := os.MkdirAll(infoPath, 0700); err != nil {
+ return fmt.Errorf("Error creating directory [%s]: %v", infoPath, err)
+ }
+ attributesPath := filepath.Join(infoPath, "attributes")
+
+ attributesFile, err := os.OpenFile(attributesPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+ if err != nil {
+ return fmt.Errorf("Error creating file [%s]: %v", attributesPath, err)
+ }
+ defer attributesFile.Close()
+
+ blobReader, err := attributesBlob.DataAsync()
+ if err != nil {
+ return err
+ }
+ defer blobReader.Close()
+
+ _, err = io.Copy(attributesFile, blobReader)
+ return err
+}
diff --git a/services/repository/push.go b/services/repository/push.go
index f031073b2e0e0..3cad6e43f4602 100644
--- a/services/repository/push.go
+++ b/services/repository/push.go
@@ -207,6 +207,12 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
log.Error("models.RemoveDeletedBranch %s/%s failed: %v", repo.ID, branch, err)
}
+ if branch == repo.DefaultBranch {
+ if err := SyncGitAttributes(gitRepo, repo.DefaultBranch); err != nil {
+ log.Error("SyncGitAttributes for %s failed: %v", repo.ID, err)
+ }
+ }
+
// Cache for big repository
if err := repo_module.CacheRef(graceful.GetManager().HammerContext(), repo, gitRepo, opts.RefFullName); err != nil {
log.Error("repo_module.CacheRef %s/%s failed: %v", repo.ID, branch, err)
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 1ca2dcc4d8144..20b956f760019 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -48,7 +48,8 @@
{{$blobHead := call $.GetBlobByPathForCommit $.HeadCommit $file.Name}}
{{$isImage := or (call $.IsBlobAnImage $blobBase) (call $.IsBlobAnImage $blobHead)}}
{{$isCsv := (call $.IsCsvFile $file)}}
- {{$showFileViewToggle := or $isImage (and (not $file.IsIncomplete) $isCsv)}}
+ {{$isRenderable := or $isImage (and (not $file.IsIncomplete) $isCsv)}}
+ {{$showFileViewToggle := or $isRenderable $file.IsGenerated}}