133728e barerepo 1mo
1
package gitread
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"bytes"
133728e barerepo 1mo
5
"context"
133728e barerepo 1mo
6
"crypto/sha256"
133728e barerepo 1mo
7
"encoding/hex"
133728e barerepo 1mo
8
"fmt"
133728e barerepo 1mo
9
"io"
133728e barerepo 1mo
10
"io/fs"
133728e barerepo 1mo
11
"path/filepath"
133728e barerepo 1mo
12
"sort"
133728e barerepo 1mo
13
"strconv"
133728e barerepo 1mo
14
"strings"
133728e barerepo 1mo
15
"time"
133728e barerepo 1mo
17
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
20
// Show reads one commit whole, because the commit page is where a person went to see it.
133728e barerepo 1mo
21
func Show(ctx context.Context, dir, rev string) (*Commit, error) {
133728e barerepo 1mo
22
if !gitx.ValidRev(rev) {
133728e barerepo 1mo
23
return nil, nil
133728e barerepo 1mo
25
out, err := gitx.Run(ctx, dir,
133728e barerepo 1mo
26
"log", "--max-count=1", "--format="+logFormat,
133728e barerepo 1mo
27
"--patch", "--unified=3", "--no-color", "--find-renames", rev, "--")
133728e barerepo 1mo
28
if err != nil {
133728e barerepo 1mo
29
return nil, err
133728e barerepo 1mo
31
commits := parseLog(out, parsePatch)
133728e barerepo 1mo
32
if len(commits) == 0 {
133728e barerepo 1mo
33
return nil, nil
133728e barerepo 1mo
35
c := commits[0]
133728e barerepo 1mo
36
markSigned(ctx, dir, commits[:1])
133728e barerepo 1mo
37
c.Signed = commits[0].Signed
133728e barerepo 1mo
38
if c.Signed && Keyring != "" {
133728e barerepo 1mo
39
// One more process, and only for a commit that carries a signature to check.
133728e barerepo 1mo
40
var buf bytes.Buffer
133728e barerepo 1mo
41
err := gitx.Pipe(ctx, dir, nil, &buf, io.Discard, []string{"GNUPGHOME=" + Keyring},
133728e barerepo 1mo
42
"log", "--max-count=1", "--format=%G?\x1e%GS\x1e%GF", rev, "--")
133728e barerepo 1mo
43
if err == nil {
133728e barerepo 1mo
44
f := strings.SplitN(strings.TrimSpace(buf.String()), "\x1e", 3)
133728e barerepo 1mo
45
c.SigState, c.Signer = f[0], f[1]
133728e barerepo 1mo
46
if len(f) == 3 {
133728e barerepo 1mo
47
c.SigKey = f[2]
133728e barerepo 1mo
51
return &c, nil
133728e barerepo 1mo
54
func patchOf(logOutput string) string {
133728e barerepo 1mo
55
if i := strings.Index(logOutput, "\x1e"); i >= 0 {
133728e barerepo 1mo
56
fields := strings.SplitN(strings.TrimPrefix(logOutput, "\x00"), "\x1e", 7)
133728e barerepo 1mo
57
if len(fields) == 7 {
133728e barerepo 1mo
58
return fields[6]
133728e barerepo 1mo
61
return ""
133728e barerepo 1mo
64
// Reachable answers "is this on the default branch", which is what a link's reader wants.
133728e barerepo 1mo
65
func Reachable(ctx context.Context, dir, rev, ref string) bool {
133728e barerepo 1mo
66
if !gitx.ValidRev(rev) || !gitx.ValidRev(ref) {
133728e barerepo 1mo
67
return false
133728e barerepo 1mo
69
_, err := gitx.Run(ctx, dir, "merge-base", "--is-ancestor", rev, ref)
133728e barerepo 1mo
70
return err == nil
133728e barerepo 1mo
73
// Entry is one row of the file tree.
133728e barerepo 1mo
74
type Entry struct {
133728e barerepo 1mo
75
Name string
133728e barerepo 1mo
76
Path string
133728e barerepo 1mo
77
Dir bool
133728e barerepo 1mo
78
Size int64
133728e barerepo 1mo
79
Subject string // the last commit that touched it
133728e barerepo 1mo
80
Author string
133728e barerepo 1mo
81
When time.Time
133728e barerepo 1mo
84
// Tree lists one directory, and its per-entry log is why this is not the landing page.
133728e barerepo 1mo
85
func Tree(ctx context.Context, dir, rev, path string) ([]Entry, error) {
133728e barerepo 1mo
86
if !gitx.ValidRev(rev) {
133728e barerepo 1mo
87
return nil, nil
133728e barerepo 1mo
89
if path != "" && !gitx.ValidPath(path) {
133728e barerepo 1mo
90
return nil, nil
133728e barerepo 1mo
92
// The tree comes from the object pool, because ls-tree is a process and this page is a listing.
133728e barerepo 1mo
93
spec := rev + ":" + path
133728e barerepo 1mo
94
objs, err := gitx.Batch(ctx, dir, []string{spec})
133728e barerepo 1mo
95
if err != nil {
133728e barerepo 1mo
96
return nil, err
133728e barerepo 1mo
98
obj := objs[spec]
133728e barerepo 1mo
99
if obj == nil || obj.Type != "tree" {
133728e barerepo 1mo
100
return nil, fmt.Errorf("%s is not a directory here", spec)
133728e barerepo 1mo
101
}
133728e barerepo 1mo
103
var entries []Entry
133728e barerepo 1mo
104
for _, row := range gitx.TreeRows(obj.Body) {
133728e barerepo 1mo
105
e := Entry{Name: row.Name, Dir: row.Mode == "40000"}
133728e barerepo 1mo
106
if path != "" {
133728e barerepo 1mo
107
e.Path = path + "/" + row.Name
133728e barerepo 1mo
108
} else {
133728e barerepo 1mo
109
e.Path = row.Name
133728e barerepo 1mo
110
}
133728e barerepo 1mo
111
entries = append(entries, e)
133728e barerepo 1mo
112
}
133728e barerepo 1mo
114
// git already sorts alphabetically, so this only puts the folders on top, as the mockup shows.
133728e barerepo 1mo
115
dirs := entries[:0:0]
133728e barerepo 1mo
116
files := entries[:0:0]
133728e barerepo 1mo
117
for _, e := range entries {
133728e barerepo 1mo
118
if e.Dir {
133728e barerepo 1mo
119
dirs = append(dirs, e)
133728e barerepo 1mo
120
} else {
133728e barerepo 1mo
121
files = append(files, e)
133728e barerepo 1mo
122
}
133728e barerepo 1mo
123
}
133728e barerepo 1mo
124
entries = append(dirs, files...)
133728e barerepo 1mo
126
// Keyed by this rev's commit and path, not the tree, because resolving a tree costs the process this saves and the branch tip filed old rows under the branch's name.
133728e barerepo 1mo
127
treeHash := ""
133728e barerepo 1mo
128
if sha := revObject(dir, rev); sha != "" {
133728e barerepo 1mo
129
treeHash = sha + ":" + path
133728e barerepo 1mo
130
}
133728e barerepo 1mo
131
if treeHash != "" {
133728e barerepo 1mo
132
if body, ok := Cache.Get("tree", treeHash); ok {
133728e barerepo 1mo
133
if cached := parseTreeLog(string(body), entries); cached != nil {
133728e barerepo 1mo
134
return cached, nil
133728e barerepo 1mo
135
}
133728e barerepo 1mo
136
}
133728e barerepo 1mo
137
}
133728e barerepo 1mo
139
var record strings.Builder
133728e barerepo 1mo
140
for i := range entries {
133728e barerepo 1mo
141
e := &entries[i]
133728e barerepo 1mo
142
out, err := gitx.Run(ctx, dir, "log", "--max-count=1",
133728e barerepo 1mo
143
"--format=%an\x1e%at\x1e%s", rev, "--", e.Path)
133728e barerepo 1mo
144
if err != nil {
133728e barerepo 1mo
145
continue
133728e barerepo 1mo
146
}
133728e barerepo 1mo
147
f := strings.SplitN(strings.TrimRight(out, "\n"), "\x1e", 3)
133728e barerepo 1mo
148
if len(f) != 3 {
133728e barerepo 1mo
149
continue
133728e barerepo 1mo
150
}
133728e barerepo 1mo
151
e.Author = f[0]
133728e barerepo 1mo
152
if secs, err := strconv.ParseInt(f[1], 10, 64); err == nil {
133728e barerepo 1mo
153
e.When = time.Unix(secs, 0)
133728e barerepo 1mo
154
}
133728e barerepo 1mo
155
e.Subject = f[2]
133728e barerepo 1mo
156
fmt.Fprintf(&record, "%s\x1e%s\x1e%d\x1e%s\n", e.Path, e.Author, e.When.Unix(), e.Subject)
133728e barerepo 1mo
157
}
133728e barerepo 1mo
158
if treeHash != "" {
133728e barerepo 1mo
159
Cache.Put("tree", treeHash, []byte(record.String()))
133728e barerepo 1mo
160
}
133728e barerepo 1mo
161
return entries, nil
133728e barerepo 1mo
164
// revObject is the immutable id a rev names, read from the ref files alone because chapter 25 will not spend a process on a cache key, and empty means do not cache.
133728e barerepo 1mo
165
func revObject(dir, rev string) string {
133728e barerepo 1mo
166
if rev == "" || rev == "HEAD" {
133728e barerepo 1mo
167
branch, err := gitx.HeadBranch(dir)
133728e barerepo 1mo
168
if err != nil {
133728e barerepo 1mo
169
return ""
133728e barerepo 1mo
170
}
133728e barerepo 1mo
171
rev = "refs/heads/" + branch
133728e barerepo 1mo
172
}
133728e barerepo 1mo
173
if len(rev) == objectIDLen && isHex(rev) {
133728e barerepo 1mo
174
return rev
133728e barerepo 1mo
175
}
133728e barerepo 1mo
176
// A proposal is typed in full, so refs/heads/refs/proposals/47 is a file nobody wrote, and git's own order settles a name that is both a tag and a branch.
133728e barerepo 1mo
177
tries := []string{"refs/" + rev, "refs/tags/" + rev, "refs/heads/" + rev}
133728e barerepo 1mo
178
if strings.HasPrefix(rev, "refs/") {
133728e barerepo 1mo
179
tries = []string{rev}
133728e barerepo 1mo
180
}
133728e barerepo 1mo
181
for _, ref := range tries {
133728e barerepo 1mo
182
if sha, err := gitx.ResolveRef(dir, ref); err == nil {
133728e barerepo 1mo
183
return sha
133728e barerepo 1mo
184
}
133728e barerepo 1mo
185
}
133728e barerepo 1mo
186
return ""
133728e barerepo 1mo
189
// ParentShort is the first parent's short id, or empty at a root commit.
133728e barerepo 1mo
190
func ParentShort(ctx context.Context, dir, rev string) string {
133728e barerepo 1mo
191
if !gitx.ValidRev(rev) {
133728e barerepo 1mo
192
return ""
133728e barerepo 1mo
193
}
133728e barerepo 1mo
194
out, err := gitx.Run(ctx, dir, "rev-parse", "--short", "--verify", "--quiet", rev+"^")
133728e barerepo 1mo
195
if err != nil {
133728e barerepo 1mo
196
return ""
133728e barerepo 1mo
197
}
133728e barerepo 1mo
198
return strings.TrimSpace(out)
133728e barerepo 1mo
201
// RepoStat is the summary of one repository on a profile page.
133728e barerepo 1mo
202
type RepoStat struct {
133728e barerepo 1mo
203
Size int64
133728e barerepo 1mo
204
Language string
133728e barerepo 1mo
205
Branch string
133728e barerepo 1mo
206
Pushed time.Time
133728e barerepo 1mo
209
// Stat measures a repository for the profile listing.
133728e barerepo 1mo
210
func Stat(ctx context.Context, dir string) RepoStat {
133728e barerepo 1mo
211
var st RepoStat
133728e barerepo 1mo
212
// A file read, because a profile lists many repositories and chapter 25 counts processes.
133728e barerepo 1mo
213
if branch, err := gitx.HeadBranch(dir); err == nil {
133728e barerepo 1mo
214
st.Branch = branch
133728e barerepo 1mo
215
}
133728e barerepo 1mo
216
if out, err := gitx.Run(ctx, dir, "for-each-ref", "--sort=-committerdate", "--count=1",
133728e barerepo 1mo
217
"--format=%(committerdate:unix)", "refs/heads"); err == nil {
133728e barerepo 1mo
218
if secs, err := strconv.ParseInt(strings.TrimSpace(out), 10, 64); err == nil {
133728e barerepo 1mo
219
st.Pushed = time.Unix(secs, 0)
133728e barerepo 1mo
220
}
133728e barerepo 1mo
221
}