snapshot of eee7b5cfc0776c68816d7fdaa8af9d7a0e0da2e6 Annotations about the code that implements koment.

internal/application/service.go

1 package application
2
3 import (
4 "errors"
5 "fmt"
6 "path"
7 "time"
8
9 "github.com/janpuc/koment/internal/anchor"
10 "github.com/janpuc/koment/internal/provenance"
11 "github.com/janpuc/koment/internal/repository"
12 "github.com/janpuc/koment/internal/store"
13 )
14
15 // Service owns local repository reads and mutations.
16 type Service struct {
17 repository repository.Repository
18 store *store.Store
19 }
20
21 // AddInput is the complete intent needed to create an annotation.
22 type AddInput struct {
23 File string
24 Excerpt string
25 Kind store.Kind
26 Body string
27 Author store.Author
28 Policy *store.Policy
29 }
30
31 // ReanchorInput moves an existing annotation without changing its identity.
32 type ReanchorInput struct {
33 ID string
34 File string
35 Excerpt string
36 }
37
38 // Mutation is the durable record and its repository-relative path.
39 type Mutation struct {
40 Record store.Annotation
41 Path string
42 Warnings []string
43 }
44
45 // NewService constructs the application service for one repository.
46 func NewService(entry repository.Repository) *Service {
47 return &Service{repository: entry, store: entry.Store()}
48 }
49
50 // Snapshot reads the repository through the shared snapshot contract.
51 func (s *Service) Snapshot() (*RepositorySnapshot, error) {
52 return BuildSnapshot(s.repository)
53 }
54
55 // Add creates one fully validated annotation record.
56 func (s *Service) Add(input AddInput) (Mutation, error) {
57 file, err := s.store.FromRoot(input.File)
58 if err != nil {
59 return Mutation{}, err
60 }
61 if err := input.Author.Validate(); err != nil {
62 return Mutation{}, err
63 }
64 if _, err := store.ParseKind(string(input.Kind)); err != nil {
65 return Mutation{}, err
66 }
67 id, err := store.NewID(time.Now())
68 if err != nil {
69 return Mutation{}, err
70 }
71 record := store.Annotation{
72 Version: store.RecordVersion, ID: id, File: file, Kind: input.Kind,
73 Body: store.WrapProse(input.Body), Created: store.Today(), Author: input.Author,
74 Policy: input.Policy,
75 }
76 if err := s.anchor(&record, file, input.Excerpt); err != nil {
77 return Mutation{}, err
78 }
79 warnings := s.captureGit(&record)
80 if err := s.store.Save(&record); err != nil {
81 return Mutation{}, err
82 }
83 return Mutation{Record: record, Path: recordPath(record.ID), Warnings: warnings}, nil
84 }
85
86 // Reanchor changes only an annotation's target.
87 func (s *Service) Reanchor(input ReanchorInput) (Mutation, error) {
88 record, err := s.store.FindByID(input.ID)
89 if err != nil {
90 return Mutation{}, err
91 }
92 file := record.File
93 if input.File != "" {
94 if file, err = s.store.FromRoot(input.File); err != nil {
95 return Mutation{}, err
96 }
97 }
98 excerpt := input.Excerpt
99 if excerpt == "" && record.Anchor.Scope == store.ScopeExcerpt {
100 excerpt = record.Anchor.Excerpt
101 }
102 moved := *record
103 if err := s.anchor(&moved, file, excerpt); err != nil {
104 return Mutation{}, err
105 }
106 moved.File = file
107 if err := s.store.Save(&moved); err != nil {
108 return Mutation{}, err
109 }
110 return Mutation{Record: moved, Path: recordPath(moved.ID)}, nil
111 }
112
113 func (s *Service) anchor(record *store.Annotation, file, excerpt string) error {
114 content, err := s.store.ReadSource(file)
115 if err != nil {
116 return fmt.Errorf("reading %s: %w", file, err)
117 }
118 if excerpt == "" {
119 record.Anchor = store.Anchor{Scope: store.ScopeFile}
120 return nil
121 }
122 captured, err := anchor.Capture(content, excerpt)
123 if err != nil {
124 lines := anchor.ExcerptLines(content, excerpt)
125 switch len(lines) {
126 case 0:
127 return fmt.Errorf("excerpt not found in %s; it must match the file verbatim", file)
128 default:
129 return fmt.Errorf("excerpt matches %d places in %s (lines %v); extend it until it is unique", len(lines), file, lines)
130 }
131 }
132 record.Anchor = captured
133 return nil
134 }
135
136 func (s *Service) captureGit(record *store.Annotation) []string {
137 context, err := provenance.Capture(s.repository.Root, record.File, record.Anchor.LastSeenLine, record.Anchor.LastSeenLine)
138 if err == nil {
139 record.Git = context
140 if provenance.WorktreeIsDirty(s.repository.Root, record.File) {
141 return []string{fmt.Sprintf("%s has uncommitted changes, so commit %s does not describe what was annotated", record.File, context.Commit[:7])}
142 }
143 return nil
144 }
145 if errors.Is(err, provenance.ErrNoGit) {
146 return []string{fmt.Sprintf("no git context recorded for %s", record.File)}
147 }
148 return []string{fmt.Sprintf("git context failed for %s: %v", record.File, err)}
149 }
150
151 func recordPath(id string) string {
152 return path.Join(store.DirName, "annotations", id+".yaml")
153 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close