snapshot of 8b9e9fcfc8b13c2d29024f6f756b28bc6851512b Annotations about the code that implements koment.

internal/application/service.go

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

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close