diff --git a/git/attr.go b/git/attr.go new file mode 100644 index 0000000000000000000000000000000000000000..1a086a0340dc78c124d405f79f5edff95ac9a300 --- /dev/null +++ b/git/attr.go @@ -0,0 +1,62 @@ +package git + +import ( + "math/rand" + "os" + "path/filepath" + "strconv" + "strings" + "time" +) + +// Attribute represents a Git attribute. +type Attribute struct { + Name string + Value string +} + +// CheckAttributes checks the attributes of the given ref and path. +func (r *Repository) CheckAttributes(ref *Reference, path string) ([]Attribute, error) { + rnd := rand.NewSource(time.Now().UnixNano()) + fn := "soft-serve-index-" + strconv.Itoa(rand.New(rnd).Int()) // nolint: gosec + tmpindex := filepath.Join(os.TempDir(), fn) + + defer os.Remove(tmpindex) // nolint: errcheck + + readTree := NewCommand("read-tree", "--reset", "-i", ref.Name().String()). + AddEnvs("GIT_INDEX_FILE=" + tmpindex) + if _, err := readTree.RunInDir(r.Path); err != nil { + return nil, err + } + + checkAttr := NewCommand("check-attr", "--cached", "-a", "--", path). + AddEnvs("GIT_INDEX_FILE=" + tmpindex) + out, err := checkAttr.RunInDir(r.Path) + if err != nil { + return nil, err + } + + return parseAttributes(path, out), nil +} + +func parseAttributes(path string, buf []byte) []Attribute { + attrs := make([]Attribute, 0) + for _, line := range strings.Split(string(buf), "\n") { + if line == "" { + continue + } + + line = strings.TrimPrefix(line, path+": ") + parts := strings.SplitN(line, ": ", 2) + if len(parts) != 2 { + continue + } + + attrs = append(attrs, Attribute{ + Name: parts[0], + Value: parts[1], + }) + } + + return attrs +} diff --git a/git/attr_test.go b/git/attr_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bda79cb369463ee04148b0cf5225a6439fd85d45 --- /dev/null +++ b/git/attr_test.go @@ -0,0 +1,91 @@ +package git + +import ( + "testing" + + "github.com/matryer/is" +) + +func TestParseAttr(t *testing.T) { + cases := []struct { + in string + file string + want []Attribute + }{ + { + in: "org/example/MyClass.java: diff: java\n", + file: "org/example/MyClass.java", + want: []Attribute{ + { + Name: "diff", + Value: "java", + }, + }, + }, + { + in: `org/example/MyClass.java: crlf: unset +org/example/MyClass.java: diff: java +org/example/MyClass.java: myAttr: set`, + file: "org/example/MyClass.java", + want: []Attribute{ + { + Name: "crlf", + Value: "unset", + }, + { + Name: "diff", + Value: "java", + }, + { + Name: "myAttr", + Value: "set", + }, + }, + }, + { + in: `org/example/MyClass.java: diff: java +org/example/MyClass.java: myAttr: set`, + file: "org/example/MyClass.java", + want: []Attribute{ + { + Name: "diff", + Value: "java", + }, + { + Name: "myAttr", + Value: "set", + }, + }, + }, + { + in: `README: caveat: unspecified`, + file: "README", + want: []Attribute{ + { + Name: "caveat", + Value: "unspecified", + }, + }, + }, + { + in: "", + file: "foo", + want: []Attribute{}, + }, + { + in: "\n", + file: "foo", + want: []Attribute{}, + }, + } + + is := is.New(t) + for _, c := range cases { + attrs := parseAttributes(c.file, []byte(c.in)) + if len(attrs) != len(c.want) { + t.Fatalf("parseAttributes(%q, %q) = %v, want %v", c.file, c.in, attrs, c.want) + } + + is.Equal(attrs, c.want) + } +} diff --git a/server/ui/pages/repo/files.go b/server/ui/pages/repo/files.go index bded6aa9632e5ef5d98ab1ef74fb49359fc776d3..a0a79352bf4fee2b564be5cb1d6b10d810c6489e 100644 --- a/server/ui/pages/repo/files.go +++ b/server/ui/pages/repo/files.go @@ -376,26 +376,54 @@ func (f *Files) selectFileCmd() tea.Msg { log.Printf("ui: files: current item is not a file") return common.ErrorMsg(errInvalidFile) } - bin, err := fi.IsBinary() - if err != nil { - f.path = filepath.Dir(f.path) - log.Printf("ui: files: error checking if file is binary %v", err) - return common.ErrorMsg(err) + + var err error + var bin bool + + r, err := f.repo.Open() + if err == nil { + attrs, err := r.CheckAttributes(f.ref, fi.Path()) + if err == nil { + for _, attr := range attrs { + if (attr.Name == "binary" && attr.Value == "set") || + (attr.Name == "text" && attr.Value == "unset") { + bin = true + break + } + } + } else { + log.Printf("ui: files: error checking attributes %v", err) + } + } else { + log.Printf("ui: files: error opening repo %v", err) + } + + if !bin { + bin, err = fi.IsBinary() + if err != nil { + f.path = filepath.Dir(f.path) + log.Printf("ui: files: error checking if file is binary %v", err) + return common.ErrorMsg(err) + } } + if bin { f.path = filepath.Dir(f.path) log.Printf("ui: files: file is binary") return common.ErrorMsg(errBinaryFile) } + c, err := fi.Bytes() if err != nil { f.path = filepath.Dir(f.path) log.Printf("ui: files: error reading file %v", err) return common.ErrorMsg(err) } + f.lastSelected = append(f.lastSelected, f.selector.Index()) return FileContentMsg{string(c), i.entry.Name()} } + log.Printf("ui: files: current item is not a file") return common.ErrorMsg(errNoFileSelected) }