Monorepo for Tangled
tangled.org
1package xrpc
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "path/filepath"
9 "time"
10
11 "github.com/bluesky-social/indigo/atproto/atclient"
12 "github.com/bluesky-social/indigo/atproto/syntax"
13
14 "github.com/go-git/go-git/v5/plumbing"
15 "github.com/go-git/go-git/v5/plumbing/object"
16 "tangled.org/core/api/tangled"
17 "tangled.org/core/knotmirror/xrpc/gitea"
18)
19
20const (
21 LastCommitCache = "last_commit:%s:%s"
22 LastCommitCacheTTL = 30 * 24 * time.Hour
23)
24
25func (x *Xrpc) GetTree(w http.ResponseWriter, r *http.Request) {
26 var (
27 repoQuery = r.URL.Query().Get("repo")
28 ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this)
29 path = r.URL.Query().Get("path") // path can be empty (defaults to root)
30 )
31 l := x.logger.With("method", "git.getTree", "repo", repoQuery, "ref", ref)
32 l.Debug("request")
33
34 repo, err := syntax.ParseDID(repoQuery)
35 if err != nil {
36 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)})
37 return
38 }
39
40 var out *tangled.GitTempGetTree_Output
41 out, err = x.getTree(r.Context(), repo, ref, path)
42 if err != nil {
43 l.Warn("local mirror failed, trying proxy", "repo", repo, "err", err)
44 if x.proxyToKnot(w, r, repo) {
45 return
46 }
47 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get tree"})
48 return
49 }
50 writeJson(w, http.StatusOK, out)
51}
52
53func (x *Xrpc) getTree(ctx context.Context, repo syntax.DID, ref, treePath string) (*tangled.GitTempGetTree_Output, error) {
54 repoPath, err := x.makeRepoPath(ctx, repo)
55 if err != nil {
56 return nil, fmt.Errorf("failed to resolve repo did: %w", err)
57 }
58 rev := ref
59 if rev == "" {
60 rev = "HEAD"
61 }
62
63 head, err := gitea.GetCommit(ctx, repoPath, rev)
64 if err != nil {
65 return nil, fmt.Errorf("get head commit: %w", err)
66 }
67
68 subRev := head.Hash.String() + "^{tree}"
69 if treePath != "" {
70 subRev = head.Hash.String() + ":" + treePath
71 }
72 subTree, err := gitea.GetTree(ctx, repoPath, subRev)
73 if err != nil {
74 return nil, fmt.Errorf("get subtree %s: %w", subRev, err)
75 }
76
77 entryPaths := make([]string, len(subTree.Entries)+1)
78 entryPaths[0] = ""
79 for i, entry := range subTree.Entries {
80 entryPaths[i+1] = entry.Name
81 }
82
83 commits, lastCommit, err := func(ctx context.Context, commit *object.Commit, treePath string, paths []string) (map[string]*object.Commit, *object.Commit, error) {
84 headRef := commit.Hash.String()
85
86 revs := make(map[string]string, len(paths))
87 var unHitPaths []string
88
89 keys := make([]string, len(paths))
90 for i, path := range paths {
91 keys[i] = fmt.Sprintf(LastCommitCache, headRef, filepath.Join(treePath, path))
92 }
93 if cached, err := x.rdb.MGet(ctx, keys...).Result(); err == nil {
94 for i, v := range cached {
95 if s, ok := v.(string); ok && s != "" {
96 revs[paths[i]] = s
97 } else {
98 unHitPaths = append(unHitPaths, paths[i])
99 }
100 }
101 } else {
102 unHitPaths = paths
103 }
104
105 if len(unHitPaths) > 0 {
106 commits, err := gitea.WalkGitLog(ctx, repoPath, headRef, treePath, unHitPaths...)
107 if err != nil {
108 return nil, nil, err
109 }
110 pipe := x.rdb.Pipeline()
111 for path, cid := range commits {
112 if cid == "" {
113 continue
114 }
115 revs[path] = cid
116 pipe.Set(ctx, fmt.Sprintf(LastCommitCache, headRef, filepath.Join(treePath, path)), cid, LastCommitCacheTTL)
117 }
118 if _, err := pipe.Exec(ctx); err != nil {
119 x.logger.Warn("git last-commit cache write failed", "err", err)
120 }
121 }
122
123 // start cat-file batch
124 batchWriter, batchReader, cancel := gitea.CatFileBatch(ctx, repoPath)
125 defer cancel()
126
127 // path -> commit map
128 commitsMap := map[string]*object.Commit{}
129 for path, commitId := range revs {
130 if commitId == headRef {
131 commitsMap[path] = commit
132 continue
133 }
134
135 if commitId == "" { // invalid commit?
136 continue
137 }
138
139 _, err := batchWriter.Write([]byte(commitId + "\n"))
140 if err != nil {
141 return nil, nil, err
142 }
143 _, typ, size, err := gitea.ReadBatchLine(batchReader)
144 if err != nil {
145 return nil, nil, err
146 }
147 if typ != "commit" {
148 if err := gitea.DiscardFull(batchReader, size+1); err != nil {
149 return nil, nil, err
150 }
151 return nil, nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitId)
152 }
153 c, err := gitea.ReadCommit(plumbing.NewHash(commitId), io.LimitReader(batchReader, size))
154 if _, err := batchReader.Discard(1); err != nil {
155 return nil, nil, err
156 }
157 commitsMap[path] = c
158 }
159
160 var treeCommit *object.Commit
161 if treePath == "" {
162 treeCommit = commit
163 } else if c, ok := commitsMap[""]; ok {
164 treeCommit = c
165 }
166
167 return commitsMap, treeCommit, nil
168 }(ctx, head, treePath, entryPaths)
169 if err != nil {
170 return nil, err
171 }
172
173 outEntries := make([]*tangled.GitTempGetTree_TreeEntry, len(subTree.Entries))
174 for i, entry := range subTree.Entries {
175 var entryLastCommit *tangled.GitTempGetTree_LastCommit
176 if commit, ok := commits[entry.Name]; ok {
177 entryLastCommit = &tangled.GitTempGetTree_LastCommit{
178 Hash: commit.Hash.String(),
179 Message: commit.Message,
180 When: commit.Author.When.Format(time.RFC3339),
181 Author: &tangled.GitTempGetTree_Signature{
182 Email: commit.Author.Email,
183 Name: commit.Author.Name,
184 },
185 }
186 }
187 outEntries[i] = &tangled.GitTempGetTree_TreeEntry{
188 Name: entry.Name,
189 Mode: entry.Mode.String(),
190 Last_commit: entryLastCommit,
191 }
192 }
193
194 var parent *string
195 var dotdot *string
196 if treePath != "" {
197 parent = &treePath
198 if dir := filepath.Dir(treePath); dir != "" {
199 dotdot = &dir
200 }
201 }
202
203 var outLastCommit *tangled.GitTempGetTree_LastCommit
204 if lastCommit != nil {
205 outLastCommit = &tangled.GitTempGetTree_LastCommit{
206 Hash: lastCommit.Hash.String(),
207 Message: lastCommit.Message,
208 When: lastCommit.Author.When.Format(time.RFC3339),
209 Author: &tangled.GitTempGetTree_Signature{
210 Email: lastCommit.Author.Email,
211 Name: lastCommit.Author.Name,
212 },
213 }
214 }
215
216 return &tangled.GitTempGetTree_Output{
217 Ref: ref,
218 Parent: parent,
219 Dotdot: dotdot,
220 Files: outEntries,
221 LastCommit: outLastCommit,
222 // TODO: remove this field entirely
223 Readme: &tangled.GitTempGetTree_Readme{
224 Filename: "",
225 Contents: "",
226 },
227 }, nil
228}