snapshot of 962fc3c82c774ab371e3fc4e84c55ef26e61268d Annotations about the code that implements koment.

internal/store/store.go

1 package store
2
3 import (
4 "bytes"
5 "crypto/rand"
6 "encoding/hex"
7 "errors"
8 "fmt"
9 "io"
10 "io/fs"
11 "log/slog"
12 "os"
13 "path"
14 "path/filepath"
15 "sort"
16 "strings"
17
18 yaml "go.yaml.in/yaml/v3"
19 )
20
21 const (
22 DirName = ".koment"
23
24 annotationsDir = "annotations"
25 recordSuffix = ".yaml"
26 yamlIndent = 2
27 )
28
29 const schemaDirective = "# yaml-language-server: $schema=" + SchemaURL + "\n"
30
31 type Store struct{ root string }
32
33 func Open(root string) *Store { return &Store{root: root} }
34
35 func (s *Store) Root() string { return s.root }
36
37 func closeRepositoryRoot(root *os.Root, returnedError *error) {
38 if err := root.Close(); err != nil {
39 *returnedError = errors.Join(*returnedError, fmt.Errorf("closing repository root: %w", err))
40 }
41 }
42
43 // FindRoot walks up from start for the directory that owns the annotations,
44 // preferring an existing .koment over the enclosing git work tree.
45 func FindRoot(start string) (string, error) {
46 directory, err := filepath.Abs(start)
47 if err != nil {
48 return "", fmt.Errorf("resolving %s: %w", start, err)
49 }
50
51 gitRoot := ""
52 for {
53 if isDir(filepath.Join(directory, DirName)) {
54 return directory, nil
55 }
56 if gitRoot == "" && exists(filepath.Join(directory, ".git")) {
57 gitRoot = directory
58 }
59 parent := filepath.Dir(directory)
60 if parent == directory {
61 break
62 }
63 directory = parent
64 }
65
66 if gitRoot != "" {
67 return gitRoot, nil
68 }
69 return "", fmt.Errorf("no %s or .git directory at or above %s", DirName, start)
70 }
71
72 // FromWorkingDirectory reads a path the way a person typing it at a shell
73 // prompt means it: relative to where they are standing.
74 func (s *Store) FromWorkingDirectory(path string) (string, error) {
75 absolute, err := filepath.Abs(path)
76 if err != nil {
77 return "", fmt.Errorf("resolving %s: %w", path, err)
78 }
79 return s.fromAbsolute(absolute, path)
80 }
81
82 // FromRoot reads a path the way an API caller means it: already relative to the
83 // repository root, wherever the koment process happens to be running.
84 func (s *Store) FromRoot(path string) (string, error) {
85 if filepath.IsAbs(path) {
86 return s.fromAbsolute(path, path)
87 }
88 return validSourcePath(filepath.ToSlash(filepath.Clean(path)))
89 }
90
91 func (s *Store) fromAbsolute(absolute, original string) (string, error) {
92 relative, err := filepath.Rel(s.root, absolute)
93 if err != nil {
94 return "", fmt.Errorf("%s is not inside %s: %w", original, s.root, err)
95 }
96 return validSourcePath(filepath.ToSlash(relative))
97 }
98
99 func validSourcePath(value string) (string, error) {
100 if strings.Contains(value, `\`) {
101 return "", fmt.Errorf("source path %s must use forward slashes", value)
102 }
103 clean := path.Clean(value)
104 switch {
105 case clean == "" || clean == ".":
106 return "", fmt.Errorf("empty source path")
107 case clean != value:
108 return "", fmt.Errorf("source path %s is not canonical; use %s", value, clean)
109 case path.IsAbs(clean) || hasDrivePrefix(clean):
110 return "", fmt.Errorf("source path %s must be relative to the repository root", value)
111 case clean == ".." || strings.HasPrefix(clean, "../"):
112 return "", fmt.Errorf("source path %s escapes the repository root", value)
113 }
114 return clean, nil
115 }
116
117 func hasDrivePrefix(value string) bool {
118 if len(value) < 2 || value[1] != ':' {
119 return false
120 }
121 letter := value[0]
122 return letter >= 'A' && letter <= 'Z' || letter >= 'a' && letter <= 'z'
123 }
124
125 func (s *Store) ReadSource(file string) (_ []byte, returnedError error) {
126 clean, err := validSourcePath(file)
127 if err != nil {
128 return nil, err
129 }
130 root, err := os.OpenRoot(s.root)
131 if err != nil {
132 return nil, fmt.Errorf("opening repository root %s: %w", s.root, err)
133 }
134 defer closeRepositoryRoot(root, &returnedError)
135 content, err := root.ReadFile(filepath.FromSlash(clean))
136 if err != nil {
137 return nil, fmt.Errorf("reading source %s: %w", clean, err)
138 }
139 return content, nil
140 }
141
142 // WriteSource atomically replaces a repository file without crossing its root.
143 func (s *Store) WriteSource(file string, content []byte) (returnedError error) {
144 clean, err := validSourcePath(file)
145 if err != nil {
146 return err
147 }
148 root, err := os.OpenRoot(s.root)
149 if err != nil {
150 return fmt.Errorf("opening repository root %s: %w", s.root, err)
151 }
152 defer closeRepositoryRoot(root, &returnedError)
153 name := filepath.FromSlash(clean)
154 information, err := root.Stat(name)
155 if err != nil {
156 return fmt.Errorf("reading permissions for %s: %w", clean, err)
157 }
158 if err := writeAtomicallyWithMode(root, name, content, information.Mode().Perm()); err != nil {
159 return fmt.Errorf("writing source %s: %w", clean, err)
160 }
161 return nil
162 }
163
164 func (s *Store) RecordPath(id string) (string, error) {
165 name, err := recordName(id)
166 if err != nil {
167 return "", err
168 }
169 return filepath.Join(s.root, name), nil
170 }
171
172 func recordName(id string) (string, error) {
173 if !ValidID(id) {
174 return "", fmt.Errorf("annotation id %q is not a canonical ULID", id)
175 }
176 return filepath.Join(DirName, annotationsDir, id+recordSuffix), nil
177 }
178
179 func (s *Store) Load(id string) (_ *Annotation, returnedError error) {
180 name, err := recordName(id)
181 if err != nil {
182 return nil, err
183 }
184 root, err := os.OpenRoot(s.root)
185 if err != nil {
186 return nil, fmt.Errorf("opening repository root %s: %w", s.root, err)
187 }
188 defer closeRepositoryRoot(root, &returnedError)
189 content, err := root.ReadFile(name)
190 if err != nil {
191 return nil, err
192 }
193 annotation, upgraded, err := decodeAnnotation(id, content)
194 if err != nil {
195 return nil, err
196 }
197 if upgraded {
198 persistUpgrade(root, name, annotation)
199 }
200 return annotation, nil
201 }
202
203 func persistUpgrade(root *os.Root, name string, annotation *Annotation) {
204 encoded, err := EncodeAnnotation(annotation)
205 if err == nil {
206 err = writeAtomically(root, name, encoded)
207 }
208 if err != nil {
209 slog.Warn("koment left a record in the v1 shape", "record", name, "error", err)
210 }
211 }
212
213 // DecodeAnnotation validates one record read from a non-filesystem source. A
214 // v1 record is upgraded in memory; only Store.Load writes the upgrade back.
215 func DecodeAnnotation(id string, content []byte) (*Annotation, error) {
216 annotation, _, err := decodeAnnotation(id, content)
217 return annotation, err
218 }
219
220 type recordShape struct {
221 APIVersion string `yaml:"apiVersion"`
222 Version *int `yaml:"version"`
223 }
224
225 func decodeAnnotation(id string, content []byte) (*Annotation, bool, error) {
226 name, err := recordName(id)
227 if err != nil {
228 return nil, false, err
229 }
230 var shape recordShape
231 if err := yaml.Unmarshal(content, &shape); err != nil {
232 return nil, false, fmt.Errorf("parsing %s: %w", name, err)
233 }
234
235 annotation, upgraded, err := decodeShape(name, shape, content)
236 if err != nil {
237 return nil, false, err
238 }
239 if err := annotation.Validate(); err != nil {
240 return nil, false, fmt.Errorf("in %s: %w", name, err)
241 }
242 if annotation.Metadata.ID != id {
243 return nil, false, fmt.Errorf("in %s: record claims id %s but filename claims %s", name, annotation.Metadata.ID, id)
244 }
245 return annotation, upgraded, nil
246 }
247
248 func decodeShape(name string, shape recordShape, content []byte) (*Annotation, bool, error) {
249 switch {
250 case shape.APIVersion == APIVersion:
251 var annotation Annotation
252 if err := decodeOneDocument(name, content, &annotation); err != nil {
253 return nil, false, err
254 }
255 return &annotation, false, nil
256 case shape.APIVersion != "":
257 return nil, false, fmt.Errorf(
258 "incompatible %s: apiVersion %q is not supported; this binary reads %s (ADR 0119)",
259 name, shape.APIVersion, APIVersion)
260 case shape.Version != nil && *shape.Version == LegacyRecordVersion:
261 var legacy legacyRecord
262 if err := decodeOneDocument(name, content, &legacy); err != nil {
263 return nil, false, err
264 }
265 upgraded := upgradeLegacy(legacy)
266 return &upgraded, true, nil
267 default:
268 return nil, false, fmt.Errorf(
269 "incompatible %s: no apiVersion; a koment record starts with `apiVersion: %s` (ADR 0119)",
270 name, APIVersion)
271 }
272 }
273
274 func decodeOneDocument(name string, content []byte, into any) error {
275 decoder := yaml.NewDecoder(bytes.NewReader(content))
276 decoder.KnownFields(true)
277 if err := decoder.Decode(into); err != nil {
278 return fmt.Errorf("parsing %s: %w", name, err)
279 }
280 var trailing any
281 if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
282 if err == nil {
283 return fmt.Errorf("parsing %s: multiple YAML documents are not allowed", name)
284 }
285 return fmt.Errorf("parsing %s after the annotation: %w", name, err)
286 }
287 return nil
288 }
289
290 func (s *Store) Save(annotation *Annotation) (returnedError error) {
291 name, err := recordName(annotation.Metadata.ID)
292 if err != nil {
293 return err
294 }
295 encoded, err := EncodeAnnotation(annotation)
296 if err != nil {
297 return err
298 }
299 root, err := os.OpenRoot(s.root)
300 if err != nil {
301 return fmt.Errorf("opening repository root %s: %w", s.root, err)
302 }
303 defer closeRepositoryRoot(root, &returnedError)
304 if err := root.MkdirAll(filepath.Join(DirName, annotationsDir), 0o755); err != nil {
305 return fmt.Errorf("creating %s: %w", filepath.Join(DirName, annotationsDir), err)
306 }
307
308 return writeAtomically(root, name, encoded)
309 }
310
311 func EncodeAnnotation(annotation *Annotation) ([]byte, error) {
312 if err := annotation.Validate(); err != nil {
313 return nil, err
314 }
315 var encoded strings.Builder
316 encoded.WriteString(schemaDirective)
317 encoder := yaml.NewEncoder(&encoded)
318 encoder.SetIndent(yamlIndent)
319 if err := encoder.Encode(annotation); err != nil {
320 return nil, fmt.Errorf("encoding annotation %s: %w", annotation.Metadata.ID, err)
321 }
322 if err := encoder.Close(); err != nil {
323 return nil, fmt.Errorf("encoding annotation %s: %w", annotation.Metadata.ID, err)
324 }
325 return []byte(encoded.String()), nil
326 }
327
328 func writeAtomically(root *os.Root, name string, content []byte) error {
329 return writeAtomicallyWithMode(root, name, content, 0o644)
330 }
331
332 func writeAtomicallyWithMode(root *os.Root, name string, content []byte, mode fs.FileMode) error {
333 var entropy [8]byte
334 if _, err := rand.Read(entropy[:]); err != nil {
335 return fmt.Errorf("creating temporary name for %s: %w", name, err)
336 }
337 temporaryName := name + "." + hex.EncodeToString(entropy[:])
338 temporary, err := root.OpenFile(temporaryName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
339 if err != nil {
340 return fmt.Errorf("creating temporary file beside %s: %w", name, err)
341 }
342 defer func() { _ = root.Remove(temporaryName) }()
343
344 if _, err := temporary.Write(content); err != nil {
345 _ = temporary.Close()
346 return fmt.Errorf("writing %s: %w", temporaryName, err)
347 }
348 if err := temporary.Close(); err != nil {
349 return fmt.Errorf("closing %s: %w", temporaryName, err)
350 }
351 if err := root.Rename(temporaryName, name); err != nil {
352 return fmt.Errorf("replacing %s: %w", name, err)
353 }
354 return nil
355 }
356
357 func (s *Store) FindByID(id string) (*Annotation, error) {
358 annotation, err := s.Load(id)
359 if errorsIsNotExist(err) {
360 return nil, fmt.Errorf("no annotation with id %s", id)
361 }
362 return annotation, err
363 }
364
365 func errorsIsNotExist(err error) bool {
366 return err != nil && os.IsNotExist(err)
367 }
368
369 func (s *Store) Remove(id string) (returnedError error) {
370 name, err := recordName(id)
371 if err != nil {
372 return err
373 }
374 root, err := os.OpenRoot(s.root)
375 if err != nil {
376 return fmt.Errorf("opening repository root %s: %w", s.root, err)
377 }
378 defer closeRepositoryRoot(root, &returnedError)
379 if err := root.Remove(name); err != nil {
380 return fmt.Errorf("removing %s: %w", name, err)
381 }
382 return nil
383 }
384
385 func (s *Store) All() (_ []Annotation, returnedError error) {
386 root, err := os.OpenRoot(s.root)
387 if err != nil {
388 return nil, fmt.Errorf("opening repository root %s: %w", s.root, err)
389 }
390 defer closeRepositoryRoot(root, &returnedError)
391 directory := path.Join(DirName, annotationsDir)
392 entries, err := fs.ReadDir(root.FS(), directory)
393 if errorsIsNotExist(err) {
394 return nil, nil
395 }
396 if err != nil {
397 return nil, fmt.Errorf("reading %s: %w", directory, err)
398 }
399
400 annotations := make([]Annotation, 0, len(entries))
401 for _, entry := range entries {
402 if entry.IsDir() {
403 return nil, fmt.Errorf("unexpected directory %s in flat annotation store", path.Join(directory, entry.Name()))
404 }
405 if !strings.HasSuffix(entry.Name(), recordSuffix) {
406 continue
407 }
408 id := strings.TrimSuffix(entry.Name(), recordSuffix)
409 annotation, err := s.Load(id)
410 if err != nil {
411 return nil, err
412 }
413 annotations = append(annotations, *annotation)
414 }
415 return annotations, nil
416 }
417
418 func (s *Store) ForFile(file string) ([]Annotation, error) {
419 clean, err := validSourcePath(file)
420 if err != nil {
421 return nil, err
422 }
423 all, err := s.All()
424 if err != nil {
425 return nil, err
426 }
427 annotations := make([]Annotation, 0)
428 for _, annotation := range all {
429 if annotation.Spec.Target.File == clean {
430 annotations = append(annotations, annotation)
431 }
432 }
433 return annotations, nil
434 }
435
436 func (s *Store) AnnotatedFiles() ([]string, error) {
437 annotations, err := s.All()
438 if err != nil {
439 return nil, err
440 }
441 unique := make(map[string]struct{}, len(annotations))
442 for _, annotation := range annotations {
443 unique[annotation.Spec.Target.File] = struct{}{}
444 }
445 files := make([]string, 0, len(unique))
446 for file := range unique {
447 files = append(files, file)
448 }
449 sort.Strings(files)
450 return files, nil
451 }
452
453 func isDir(path string) bool {
454 info, err := os.Stat(path)
455 return err == nil && info.IsDir()
456 }
457
458 func exists(path string) bool {
459 _, err := os.Stat(path)
460 return err == nil
461 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close