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
163
164
165
166
167
168
169
170
171
|
package furgit
import (
"encoding/hex"
"fmt"
"os"
"path/filepath"
"sync"
"codeberg.org/lindenii/furgit/config"
)
// Repository represents a Git repository.
//
// It is safe to access the same Repository from multiple goroutines
// without additional synchronization.
//
// Objects derived from a Repository must not be used after the Repository
// has been closed.
type Repository struct {
rootPath string
hashAlgo hashAlgorithm
packIdxOnce sync.Once
packIdx []*packIndex
packIdxErr error
packFiles map[string]*packFile
packFilesMu sync.RWMutex
commitGraphOnce sync.Once
commitGraph *commitGraph
commitGraphErr error
closeOnce sync.Once
}
// OpenRepository opens the repository at the provided path.
//
// The path is expected to be the actual repository directory, i.e.,
// the repository itself for bare repositories, or the .git
// subdirectory for non-bare repositories.
func OpenRepository(path string) (*Repository, error) {
fi, err := os.Stat(path)
if err != nil {
return nil, err
}
if !fi.IsDir() {
return nil, ErrInvalidObject
}
cfgPath := filepath.Join(path, "config")
f, err := os.Open(cfgPath)
if err != nil {
return nil, fmt.Errorf("furgit: unable to open config: %w", err)
}
defer func() {
_ = f.Close()
}()
cfg, err := config.ParseConfig(f)
if err != nil {
return nil, fmt.Errorf("furgit: failed to parse config: %w", err)
}
algo := cfg.Get("extensions", "", "objectformat")
if algo == "" {
algo = "sha1"
}
hashAlgo, ok := parseHashAlgorithm(algo)
if !ok {
return nil, fmt.Errorf("furgit: unsupported hash algorithm %q", algo)
}
return &Repository{
rootPath: path,
hashAlgo: hashAlgo,
packFiles: make(map[string]*packFile),
}, nil
}
// Close closes the repository, releasing any resources associated with it.
//
// It is safe to call Close multiple times; subsequent calls will have no
// effect.
//
// Close invalidates any objects derived from the Repository as it;
// using them may cause segmentation faults or other undefined behavior.
func (repo *Repository) Close() error {
var closeErr error
repo.closeOnce.Do(func() {
repo.packFilesMu.Lock()
for key, pf := range repo.packFiles {
err := pf.Close()
if err != nil && closeErr == nil {
closeErr = err
}
delete(repo.packFiles, key)
}
repo.packFilesMu.Unlock()
if len(repo.packIdx) > 0 {
for _, idx := range repo.packIdx {
err := idx.Close()
if err != nil && closeErr == nil {
closeErr = err
}
}
}
if repo.commitGraph != nil {
err := repo.commitGraph.Close()
if err != nil && closeErr == nil {
closeErr = err
}
}
})
return closeErr
}
// repoPath joins the root with a relative path.
func (repo *Repository) repoPath(rel string) string {
return filepath.Join(repo.rootPath, rel)
}
// ParseHash converts a hex string into a Hash, validating
// it matches the repository's hash size.
func (repo *Repository) ParseHash(s string) (Hash, error) {
var id Hash
if len(s)%2 != 0 {
return id, fmt.Errorf("furgit: invalid hash length %d, it has to be even at the very least", len(s))
}
expectedLen := repo.hashAlgo.Size() * 2
if len(s) != expectedLen {
return id, fmt.Errorf("furgit: hash length mismatch: got %d chars, expected %d for hash size %d", len(s), expectedLen, repo.hashAlgo.Size())
}
data, err := hex.DecodeString(s)
if err != nil {
return id, fmt.Errorf("furgit: decode hash: %w", err)
}
copy(id.data[:], data)
id.algo = repo.hashAlgo
return id, nil
}
// computeRawHash computes a hash from raw data using the repository's hash algorithm.
func (repo *Repository) computeRawHash(data []byte) Hash {
return repo.hashAlgo.Sum(data)
}
// verifyRawObject verifies a raw object against its expected hash.
func (repo *Repository) verifyRawObject(buf []byte, want Hash) bool { //nolint:unused
if want.algo != repo.hashAlgo {
return false
}
return repo.computeRawHash(buf) == want
}
// verifyTypedObject verifies a typed object against its expected hash.
func (repo *Repository) verifyTypedObject(ty ObjectType, body []byte, want Hash) bool { //nolint:unused
if want.algo != repo.hashAlgo {
return false
}
header, err := headerForType(ty, body)
if err != nil {
return false
}
raw := make([]byte, len(header)+len(body))
copy(raw, header)
copy(raw[len(header):], body)
return repo.computeRawHash(raw) == want
}
|