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

internal/provenance/provenance.go

1 // Package provenance captures where an annotation came from: the git state it
2 // was written against, and who wrote it.
3 package provenance
4
5 import (
6 "errors"
7 "os/exec"
8 "strings"
9
10 "github.com/koment-dev/koment/internal/store"
11 )
12
13 // ErrNoGit means git could not answer, which is a normal outcome rather than a
14 // failure: koment works in a directory that is not a repository.
15 var ErrNoGit = errors.New("no usable git context")
16
17 // Capture records the commit an annotation was written against. It reports
18 // ErrNoGit rather than guessing when git cannot answer, because a fabricated
19 // commit reference is worse than an absent one.
20 func Capture(root, file string, line, endLine int) (*store.GitContext, error) {
21 commit, err := Head(root)
22 if err != nil {
23 return nil, err
24 }
25 if tracked, err := git(root, "ls-files", "--error-unmatch", "--", file); err != nil || tracked == "" {
26 return nil, ErrNoGit
27 }
28
29 return &store.GitContext{
30 Commit: commit,
31 Path: file,
32 Line: line,
33 EndLine: endLineOrZero(line, endLine),
34 }, nil
35 }
36
37 func endLineOrZero(line, endLine int) int {
38 if endLine == line {
39 return 0
40 }
41 return endLine
42 }
43
44 // Head is the full commit a record is stamped against. A record never stores
45 // the abbreviated form, because an abbreviation stops being unique as history
46 // grows.
47 func Head(root string) (string, error) {
48 commit, err := git(root, "rev-parse", "HEAD")
49 if err != nil || commit == "" {
50 return "", ErrNoGit
51 }
52 return commit, nil
53 }
54
55 // HeadCommit is the abbreviated commit a snapshot was taken at. It reports
56 // ErrNoGit rather than an empty string, so a caller that needs to name the
57 // commit cannot mistake "not a repository" for "no commit".
58 func HeadCommit(root string) (string, error) {
59 commit, err := git(root, "rev-parse", "--short", "HEAD")
60 if err != nil || commit == "" {
61 return "", ErrNoGit
62 }
63 return commit, nil
64 }
65
66 // WorktreeIsDirty reports whether the file has uncommitted changes, which makes
67 // the captured commit describe something other than what was annotated.
68 func WorktreeIsDirty(root, file string) bool {
69 changed, err := git(root, "status", "--porcelain", "--", file)
70 return err == nil && changed != ""
71 }
72
73 // TreeIsDirty is the same question asked of the whole repository, which is what
74 // a snapshot of every file has to ask before naming a commit.
75 func TreeIsDirty(root string) bool {
76 changed, err := git(root, "status", "--porcelain")
77 return err == nil && changed != ""
78 }
79
80 // IdentityFromGit reads the committer identity git would use. It is a claim,
81 // not a proof, and says so through its source.
82 func IdentityFromGit(root string) (*store.Author, error) {
83 name, err := git(root, "config", "user.name")
84 if err != nil || name == "" {
85 return nil, errors.New("no git user.name configured; set one or pass --author")
86 }
87 email, emailErr := git(root, "config", "user.email")
88 if emailErr != nil {
89 email = ""
90 }
91
92 return &store.Author{
93 Name: name,
94 Email: email,
95 Kind: store.AuthorHuman,
96 Source: store.FromGitConfig,
97 }, nil
98 }
99
100 // ParseAuthor reads an explicit "Name <email>" identity.
101 func ParseAuthor(text string, kind store.AuthorKind) (*store.Author, error) {
102 name, email, found := strings.Cut(strings.TrimSpace(text), "<")
103 author := &store.Author{
104 Name: strings.TrimSpace(name),
105 Kind: kind,
106 Source: store.FromExplicit,
107 }
108 if found {
109 author.Email = strings.TrimSpace(strings.TrimSuffix(email, ">"))
110 }
111 if author.Name == "" {
112 return nil, errors.New(`--author must look like "Name" or "Name <email>"`)
113 }
114 return author, nil
115 }
116
117 func git(root string, args ...string) (string, error) {
118 command := exec.Command("git", args...)
119 command.Dir = root
120 output, err := command.Output()
121 if err != nil {
122 return "", err
123 }
124 return strings.TrimSpace(string(output)), nil
125 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close