snapshot of eee7b5cfc0776c68816d7fdaa8af9d7a0e0da2e6 Annotations about the code that implements koment.

internal/application/snapshot.go

1 package application
2
3 import (
4 "errors"
5 "io/fs"
6 "sort"
7 "strings"
8 "time"
9
10 "github.com/janpuc/koment/internal/anchor"
11 "github.com/janpuc/koment/internal/provenance"
12 "github.com/janpuc/koment/internal/repository"
13 "github.com/janpuc/koment/internal/store"
14 )
15
16 // RepositorySnapshot is one internally consistent read of a repository.
17 type RepositorySnapshot struct {
18 Repository RepositoryIdentity
19 Commit string
20 Dirty bool
21 GeneratedAt time.Time
22 Files []FileSnapshot
23 }
24
25 // RepositoryIdentity is stable presentation metadata for a snapshot.
26 type RepositoryIdentity struct {
27 ID string
28 Name string
29 CloneURL string
30 DefaultBranch string
31 }
32
33 // FileSnapshot contains source and resolved annotations from the same read.
34 type FileSnapshot struct {
35 Path string
36 Content []byte
37 Exists bool
38 Annotations []AnnotationView
39 }
40
41 // AnnotationView is the common record and resolution presented by every reader.
42 type AnnotationView struct {
43 Record store.Annotation
44 Status anchor.Status
45 Line int
46 Occurrences int
47 Warning string
48 }
49
50 // SnapshotInput is a complete repository read from one source revision.
51 type SnapshotInput struct {
52 Repository RepositoryIdentity
53 Commit string
54 Dirty bool
55 GeneratedAt time.Time
56 Records []store.Annotation
57 Sources map[string][]byte
58 }
59
60 // BuildSnapshot reads every annotation and annotated source file once.
61 func BuildSnapshot(entry repository.Repository) (*RepositorySnapshot, error) {
62 annotations := entry.Store()
63 records, err := annotations.All()
64 if err != nil {
65 return nil, err
66 }
67 sources := make(map[string][]byte)
68 for _, record := range records {
69 if _, loaded := sources[record.File]; loaded {
70 continue
71 }
72 content, readErr := annotations.ReadSource(record.File)
73 switch {
74 case errors.Is(readErr, fs.ErrNotExist):
75 case readErr != nil:
76 return nil, readErr
77 default:
78 sources[record.File] = content
79 }
80 }
81
82 input := SnapshotInput{
83 Repository: RepositoryIdentity{
84 ID: entry.ID, Name: entry.Display(), CloneURL: entry.CloneURL,
85 DefaultBranch: entry.DefaultBranch,
86 },
87 GeneratedAt: time.Now().UTC(), Records: records, Sources: sources,
88 }
89 if commit, commitErr := provenance.HeadCommit(entry.Root); commitErr == nil {
90 input.Commit = commit
91 input.Dirty = provenance.TreeIsDirty(entry.Root)
92 } else if !errors.Is(commitErr, provenance.ErrNoGit) {
93 return nil, commitErr
94 }
95 return AssembleSnapshot(input)
96 }
97
98 // AssembleSnapshot resolves a complete source revision without performing I/O.
99 func AssembleSnapshot(input SnapshotInput) (*RepositorySnapshot, error) {
100 records := append([]store.Annotation(nil), input.Records...)
101 sort.Slice(records, func(left, right int) bool { return records[left].ID < records[right].ID })
102 grouped := make(map[string][]store.Annotation)
103 seen := make(map[string]struct{}, len(records))
104 for _, record := range records {
105 if err := record.Validate(); err != nil {
106 return nil, err
107 }
108 if _, duplicate := seen[record.ID]; duplicate {
109 return nil, errors.New("duplicate annotation id " + record.ID)
110 }
111 seen[record.ID] = struct{}{}
112 grouped[record.File] = append(grouped[record.File], record)
113 }
114 paths := make([]string, 0, len(grouped))
115 for path := range grouped {
116 paths = append(paths, path)
117 }
118 sort.Strings(paths)
119 generatedAt := input.GeneratedAt
120 if generatedAt.IsZero() {
121 generatedAt = time.Now().UTC()
122 }
123 snapshot := &RepositorySnapshot{
124 Repository: input.Repository, Commit: input.Commit, Dirty: input.Dirty,
125 GeneratedAt: generatedAt,
126 }
127
128 for _, path := range paths {
129 content, exists := input.Sources[path]
130 file := FileSnapshot{Path: path, Exists: exists}
131 if !exists {
132 for _, record := range grouped[path] {
133 file.Annotations = append(file.Annotations, describe(anchor.ResolveOrphaned(record)))
134 }
135 } else {
136 file.Content = append([]byte(nil), content...)
137 for _, record := range grouped[path] {
138 file.Annotations = append(file.Annotations, describe(anchor.Resolve(record, file.Content)))
139 }
140 }
141 snapshot.Files = append(snapshot.Files, file)
142 }
143 return snapshot, nil
144 }
145
146 func describe(resolution anchor.Resolution) AnnotationView {
147 return AnnotationView{
148 Record: resolution.Annotation, Status: resolution.Status, Line: resolution.Line,
149 Occurrences: resolution.Occurrences, Warning: WarningFor(resolution.Status),
150 }
151 }
152
153 // WarningFor is the single stale-record warning policy for every surface.
154 func WarningFor(status anchor.Status) string {
155 switch status {
156 case anchor.StatusAmbiguous:
157 return "STALE: the excerpt matches several places and its context does not identify one. Treat it as history until someone explicitly reanchors it."
158 case anchor.StatusDrifted:
159 return "STALE: the annotated code changed. Treat this as history until someone explicitly reanchors it."
160 case anchor.StatusOrphaned:
161 return "STALE: the annotated file no longer exists. Treat this as history only."
162 default:
163 return ""
164 }
165 }
166
167 // File returns one annotated file from the snapshot.
168 func (s *RepositorySnapshot) File(path string) (FileSnapshot, bool) {
169 for _, file := range s.Files {
170 if file.Path == path {
171 return file, true
172 }
173 }
174 return FileSnapshot{}, false
175 }
176
177 // Search returns annotations whose record fields contain the query.
178 func (s *RepositorySnapshot) Search(query string) []AnnotationView {
179 needle := strings.ToLower(strings.TrimSpace(query))
180 if needle == "" {
181 return nil
182 }
183 var matches []AnnotationView
184 for _, file := range s.Files {
185 for _, annotation := range file.Annotations {
186 record := annotation.Record
187 haystack := strings.ToLower(strings.Join([]string{
188 record.ID, record.File, string(record.Kind), record.Body,
189 record.Author.Name, record.Author.Email, record.Author.Account,
190 }, "\n"))
191 if strings.Contains(haystack, needle) {
192 matches = append(matches, annotation)
193 }
194 }
195 }
196 return matches
197 }
198
199 // Counts returns resolution counts for the repository.
200 func (s *RepositorySnapshot) Counts() map[anchor.Status]int {
201 counts := map[anchor.Status]int{}
202 for _, file := range s.Files {
203 for _, annotation := range file.Annotations {
204 counts[annotation.Status]++
205 }
206 }
207 return counts
208 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close