aboutsummaryrefslogtreecommitdiffstats
path: root/utils/merkletrie/filesystem/node.go
blob: fc8f191ea74b7c82faa5abf70d4300d0edfd2c5b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
package filesystem

import (
	"io"
	"os"
	"path/filepath"

	"gopkg.in/src-d/go-billy.v2"
	"gopkg.in/src-d/go-git.v4/plumbing"
	"gopkg.in/src-d/go-git.v4/plumbing/filemode"
	"gopkg.in/src-d/go-git.v4/utils/merkletrie/noder"
)

var ignore = map[string]bool{
	".git": true,
}

// The node represents a file or a directory in a billy.Filesystem. It
// implements the interface noder.Noder of merkletrie package.
//
// This implementation implements a "standard" hash method being able to be
// compared with any other noder.Noder implementation inside of go-git.
type node struct {
	fs         billy.Filesystem
	submodules map[string]plumbing.Hash

	path     string
	hash     []byte
	children []noder.Noder
	isDir    bool
}

// NewRootNode returns the root node based on a given billy.Filesystem.
//
// In order to provide the submodule hash status, a map[string]plumbing.Hash
// should be provided where the key is the path of the submodule and the commit
// of the submodule HEAD
func NewRootNode(
	fs billy.Filesystem,
	submodules map[string]plumbing.Hash,
) noder.Noder {
	return &node{fs: fs, submodules: submodules, isDir: true}
}

// Hash the hash of a filesystem is the result of concatenating the computed
// plumbing.Hash of the file as a Blob and its plumbing.FileMode; that way the
// difftree algorithm will detect changes in the contents of files and also in
// their mode.
//
// The hash of a directory is always a 24-bytes slice of zero values
func (n *node) Hash() []byte {
	return n.hash
}

func (n *node) Name() string {
	return filepath.Base(n.path)
}

func (n *node) IsDir() bool {
	return n.isDir
}

func (n *node) Children() ([]noder.Noder, error) {
	if err := n.calculateChildren(); err != nil {
		return nil, err
	}

	return n.children, nil
}

func (n *node) NumChildren() (int, error) {
	if err := n.calculateChildren(); err != nil {
		return -1, err
	}

	return len(n.children), nil
}

func (n *node) calculateChildren() error {
	if len(n.children) != 0 {
		return nil
	}

	files, err := n.fs.ReadDir(n.path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil
		}

		return nil
	}

	for _, file := range files {
		if _, ok := ignore[file.Name()]; ok {
			continue
		}

		c, err := n.newChildNode(file)
		if err != nil {
			return err
		}

		n.children = append(n.children, c)
	}

	return nil
}

func (n *node) newChildNode(file billy.FileInfo) (*node, error) {
	path := filepath.Join(n.path, file.Name())

	hash, err := n.calculateHash(path, file)
	if err != nil {
		return nil, err
	}

	node := &node{
		fs:         n.fs,
		submodules: n.submodules,

		path:  path,
		hash:  hash,
		isDir: file.IsDir(),
	}

	if hash, isSubmodule := n.submodules[path]; isSubmodule {
		node.hash = append(hash[:], filemode.Submodule.Bytes()...)
		node.isDir = false
	}

	return node, nil
}

func (n *node) calculateHash(path string, file billy.FileInfo) ([]byte, error) {
	if file.IsDir() {
		return make([]byte, 24), nil
	}

	f, err := n.fs.Open(path)
	if err != nil {
		return nil, err
	}

	defer f.Close()

	h := plumbing.NewHasher(plumbing.BlobObject, file.Size())
	if _, err := io.Copy(h, f); err != nil {
		return nil, err
	}

	mode, err := filemode.NewFromOSFileMode(file.Mode())
	if err != nil {
		return nil, err
	}

	hash := h.Sum()
	return append(hash[:], mode.Bytes()...), nil
}

func (n *node) String() string {
	return n.path
}