Monorepo for Tangled tangled.org
2

Configure Feed

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

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}