Diff
1diff --git a/git/attr.go b/git/attr.go
2new file mode 100644
3index 0000000000000000000000000000000000000000..1a086a0340dc78c124d405f79f5edff95ac9a300
4--- /dev/null
5+++ b/git/attr.go
6@@ -0,0 +1,62 @@
7+package git
8+
9+import (
10+ "math/rand"
11+ "os"
12+ "path/filepath"
13+ "strconv"
14+ "strings"
15+ "time"
16+)
17+
18+// Attribute represents a Git attribute.
19+type Attribute struct {
20+ Name string
21+ Value string
22+}
23+
24+// CheckAttributes checks the attributes of the given ref and path.
25+func (r *Repository) CheckAttributes(ref *Reference, path string) ([]Attribute, error) {
26+ rnd := rand.NewSource(time.Now().UnixNano())
27+ fn := "soft-serve-index-" + strconv.Itoa(rand.New(rnd).Int()) // nolint: gosec
28+ tmpindex := filepath.Join(os.TempDir(), fn)
29+
30+ defer os.Remove(tmpindex) // nolint: errcheck
31+
32+ readTree := NewCommand("read-tree", "--reset", "-i", ref.Name().String()).
33+ AddEnvs("GIT_INDEX_FILE=" + tmpindex)
34+ if _, err := readTree.RunInDir(r.Path); err != nil {
35+ return nil, err
36+ }
37+
38+ checkAttr := NewCommand("check-attr", "--cached", "-a", "--", path).
39+ AddEnvs("GIT_INDEX_FILE=" + tmpindex)
40+ out, err := checkAttr.RunInDir(r.Path)
41+ if err != nil {
42+ return nil, err
43+ }
44+
45+ return parseAttributes(path, out), nil
46+}
47+
48+func parseAttributes(path string, buf []byte) []Attribute {
49+ attrs := make([]Attribute, 0)
50+ for _, line := range strings.Split(string(buf), "\n") {
51+ if line == "" {
52+ continue
53+ }
54+
55+ line = strings.TrimPrefix(line, path+": ")
56+ parts := strings.SplitN(line, ": ", 2)
57+ if len(parts) != 2 {
58+ continue
59+ }
60+
61+ attrs = append(attrs, Attribute{
62+ Name: parts[0],
63+ Value: parts[1],
64+ })
65+ }
66+
67+ return attrs
68+}
69diff --git a/git/attr_test.go b/git/attr_test.go
70new file mode 100644
71index 0000000000000000000000000000000000000000..bda79cb369463ee04148b0cf5225a6439fd85d45
72--- /dev/null
73+++ b/git/attr_test.go
74@@ -0,0 +1,91 @@
75+package git
76+
77+import (
78+ "testing"
79+
80+ "github.com/matryer/is"
81+)
82+
83+func TestParseAttr(t *testing.T) {
84+ cases := []struct {
85+ in string
86+ file string
87+ want []Attribute
88+ }{
89+ {
90+ in: "org/example/MyClass.java: diff: java\n",
91+ file: "org/example/MyClass.java",
92+ want: []Attribute{
93+ {
94+ Name: "diff",
95+ Value: "java",
96+ },
97+ },
98+ },
99+ {
100+ in: `org/example/MyClass.java: crlf: unset
101+org/example/MyClass.java: diff: java
102+org/example/MyClass.java: myAttr: set`,
103+ file: "org/example/MyClass.java",
104+ want: []Attribute{
105+ {
106+ Name: "crlf",
107+ Value: "unset",
108+ },
109+ {
110+ Name: "diff",
111+ Value: "java",
112+ },
113+ {
114+ Name: "myAttr",
115+ Value: "set",
116+ },
117+ },
118+ },
119+ {
120+ in: `org/example/MyClass.java: diff: java
121+org/example/MyClass.java: myAttr: set`,
122+ file: "org/example/MyClass.java",
123+ want: []Attribute{
124+ {
125+ Name: "diff",
126+ Value: "java",
127+ },
128+ {
129+ Name: "myAttr",
130+ Value: "set",
131+ },
132+ },
133+ },
134+ {
135+ in: `README: caveat: unspecified`,
136+ file: "README",
137+ want: []Attribute{
138+ {
139+ Name: "caveat",
140+ Value: "unspecified",
141+ },
142+ },
143+ },
144+ {
145+ in: "",
146+ file: "foo",
147+ want: []Attribute{},
148+ },
149+ {
150+ in: "\n",
151+ file: "foo",
152+ want: []Attribute{},
153+ },
154+ }
155+
156+ is := is.New(t)
157+ for _, c := range cases {
158+ attrs := parseAttributes(c.file, []byte(c.in))
159+ if len(attrs) != len(c.want) {
160+ t.Fatalf("parseAttributes(%q, %q) = %v, want %v", c.file, c.in, attrs, c.want)
161+ }
162+
163+ is.Equal(attrs, c.want)
164+ }
165+}
166diff --git a/server/ui/pages/repo/files.go b/server/ui/pages/repo/files.go
167index bded6aa9632e5ef5d98ab1ef74fb49359fc776d3..a0a79352bf4fee2b564be5cb1d6b10d810c6489e 100644
168--- a/server/ui/pages/repo/files.go
169+++ b/server/ui/pages/repo/files.go
170@@ -376,26 +376,54 @@ func (f *Files) selectFileCmd() tea.Msg {
171 log.Printf("ui: files: current item is not a file")
172 return common.ErrorMsg(errInvalidFile)
173 }
174- bin, err := fi.IsBinary()
175- if err != nil {
176- f.path = filepath.Dir(f.path)
177- log.Printf("ui: files: error checking if file is binary %v", err)
178- return common.ErrorMsg(err)
179+
180+ var err error
181+ var bin bool
182+
183+ r, err := f.repo.Open()
184+ if err == nil {
185+ attrs, err := r.CheckAttributes(f.ref, fi.Path())
186+ if err == nil {
187+ for _, attr := range attrs {
188+ if (attr.Name == "binary" && attr.Value == "set") ||
189+ (attr.Name == "text" && attr.Value == "unset") {
190+ bin = true
191+ break
192+ }
193+ }
194+ } else {
195+ log.Printf("ui: files: error checking attributes %v", err)
196+ }
197+ } else {
198+ log.Printf("ui: files: error opening repo %v", err)
199+ }
200+
201+ if !bin {
202+ bin, err = fi.IsBinary()
203+ if err != nil {
204+ f.path = filepath.Dir(f.path)
205+ log.Printf("ui: files: error checking if file is binary %v", err)
206+ return common.ErrorMsg(err)
207+ }
208 }
209+
210 if bin {
211 f.path = filepath.Dir(f.path)
212 log.Printf("ui: files: file is binary")
213 return common.ErrorMsg(errBinaryFile)
214 }
215+
216 c, err := fi.Bytes()
217 if err != nil {
218 f.path = filepath.Dir(f.path)
219 log.Printf("ui: files: error reading file %v", err)
220 return common.ErrorMsg(err)
221 }
222+
223 f.lastSelected = append(f.lastSelected, f.selector.Index())
224 return FileContentMsg{string(c), i.entry.Name()}
225 }
226+
227 log.Printf("ui: files: current item is not a file")
228 return common.ErrorMsg(errNoFileSelected)
229 }