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

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close