1package gitattr
2
3import (
4	"strings"
5
6	"github.com/git-lfs/gitobj/v2"
7)
8
9// Tree represents the .gitattributes file at one layer of the tree in a Git
10// repository.
11type Tree struct {
12	// Lines are the lines of the .gitattributes at this level of the tree.
13	Lines []*Line
14	// Children are the named child directories in the repository.
15	Children map[string]*Tree
16}
17
18// New constructs a *Tree starting at the given tree "t" and reading objects
19// from the given ObjectDatabase. If a tree was not able to be read, an error
20// will be propagated up accordingly.
21func New(db *gitobj.ObjectDatabase, t *gitobj.Tree) (*Tree, error) {
22	children := make(map[string]*Tree)
23	lines, _, err := linesInTree(db, t)
24	if err != nil {
25		return nil, err
26	}
27
28	for _, entry := range t.Entries {
29		if entry.Type() != gitobj.TreeObjectType {
30			continue
31		}
32
33		// For every entry in the current tree, parse its sub-trees to
34		// see if they might contain a .gitattributes.
35		t, err := db.Tree(entry.Oid)
36		if err != nil {
37			return nil, err
38		}
39
40		at, err := New(db, t)
41		if err != nil {
42			return nil, err
43		}
44
45		if len(at.Children) > 0 || len(at.Lines) > 0 {
46			// Only include entries that have either (1) a
47			// .gitattributes in their tree, or (2) a .gitattributes
48			// in a sub-tree.
49			children[entry.Name] = at
50		}
51	}
52
53	return &Tree{
54		Lines:    lines,
55		Children: children,
56	}, nil
57}
58
59// linesInTree parses a given tree's .gitattributes and returns a slice of lines
60// in that .gitattributes, or an error. If no .gitattributes blob was found,
61// return nil.
62func linesInTree(db *gitobj.ObjectDatabase, t *gitobj.Tree) ([]*Line, string, error) {
63	var at int = -1
64	for i, e := range t.Entries {
65		if e.Name == ".gitattributes" {
66			at = i
67			break
68		}
69	}
70
71	if at < 0 {
72		return nil, "", nil
73	}
74
75	blob, err := db.Blob(t.Entries[at].Oid)
76	if err != nil {
77		return nil, "", err
78	}
79	defer blob.Close()
80
81	return ParseLines(blob.Contents)
82}
83
84// Applied returns a slice of attributes applied to the given path, relative to
85// the receiving tree. It traverse through sub-trees in a topological ordering,
86// if there are relevant .gitattributes matching that path.
87func (t *Tree) Applied(to string) []*Attr {
88	var attrs []*Attr
89	for _, line := range t.Lines {
90		if line.Pattern.Match(to) {
91			attrs = append(attrs, line.Attrs...)
92		}
93	}
94
95	splits := strings.SplitN(to, "/", 2)
96	if len(splits) == 2 {
97		car, cdr := splits[0], splits[1]
98		if child, ok := t.Children[car]; ok {
99			attrs = append(attrs, child.Applied(cdr)...)
100		}
101	}
102
103	return attrs
104}
105