This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

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