aboutsummaryrefslogtreecommitdiff
path: root/repo.go
blob: 53f7f5cda28d8bcd883c512d6a3d28c54d0e2499 (about) (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
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
}