forked from
tangled.org/core
Monorepo for Tangled
1package xrpc
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "path/filepath"
8 "runtime/pprof"
9 "time"
10 "unicode/utf8"
11
12 "github.com/bluesky-social/indigo/atproto/atclient"
13 "github.com/bluesky-social/indigo/atproto/syntax"
14 "tangled.org/core/api/tangled"
15 "tangled.org/core/appview/pages/markup"
16 "tangled.org/core/knotserver/git"
17 "tangled.org/core/types"
18)
19
20func (x *Xrpc) GetTree(w http.ResponseWriter, r *http.Request) {
21 var (
22 repoQuery = r.URL.Query().Get("repo")
23 ref = r.URL.Query().Get("ref") // ref can be empty (git.Open handles this)
24 path = r.URL.Query().Get("path") // path can be empty (defaults to root)
25 )
26 l := x.logger.With("method", "git.getTree", "repo", repoQuery, "ref", ref)
27 l.Debug("request")
28
29 repo, err := syntax.ParseATURI(repoQuery)
30 if err != nil || repo.RecordKey() == "" {
31 writeJson(w, http.StatusBadRequest, atclient.ErrorBody{Name: "BadRequest", Message: fmt.Sprintf("repo parameter invalid: %s", repoQuery)})
32 return
33 }
34
35 var out *tangled.GitTempGetTree_Output
36 pprof.Do(r.Context(), pprof.Labels("repo", repo.String()), func(ctx context.Context) {
37 out, err = x.getTree(ctx, repo, ref, path)
38 })
39 if err != nil {
40 l.Warn("local mirror failed, trying proxy", "repo", repo, "err", err)
41 if x.proxyToKnot(w, r, repo) {
42 return
43 }
44 writeJson(w, http.StatusInternalServerError, atclient.ErrorBody{Name: "InternalServerError", Message: "failed to get tree"})
45 return
46 }
47 writeJson(w, http.StatusOK, out)
48}
49
50func (x *Xrpc) getTree(ctx context.Context, repo syntax.ATURI, ref, path string) (*tangled.GitTempGetTree_Output, error) {
51 repoPath, err := x.makeRepoPath(ctx, repo)
52 if err != nil {
53 return nil, fmt.Errorf("failed to resolve repo at-uri: %w", err)
54 }
55
56 gr, err := git.Open(repoPath, ref)
57 if err != nil {
58 return nil, fmt.Errorf("opening git repo: %w", err)
59 }
60
61 files, err := gr.FileTree(ctx, path)
62 if err != nil {
63 return nil, fmt.Errorf("reading file tree: %w", err)
64 }
65
66 // if any of these files are a readme candidate, pass along its blob contents too
67 var readmeFileName string
68 var readmeContents string
69 for _, file := range files {
70 if markup.IsReadmeFile(file.Name) {
71 contents, err := gr.RawContent(filepath.Join(path, file.Name))
72 if err != nil {
73 x.logger.Error("failed to read contents of file", "path", path, "file", file.Name)
74 }
75
76 if utf8.Valid(contents) {
77 readmeFileName = file.Name
78 readmeContents = string(contents)
79 break
80 }
81 }
82 }
83
84 // convert NiceTree -> tangled.RepoTempGetTree_TreeEntry
85 treeEntries := make([]*tangled.GitTempGetTree_TreeEntry, len(files))
86 for i, file := range files {
87 entry := &tangled.GitTempGetTree_TreeEntry{
88 Name: file.Name,
89 Mode: file.Mode,
90 Size: file.Size,
91 }
92 if file.LastCommit != nil {
93 entry.Last_commit = &tangled.GitTempGetTree_LastCommit{
94 Hash: file.LastCommit.Hash.String(),
95 Message: file.LastCommit.Message,
96 When: file.LastCommit.When.Format(time.RFC3339),
97 }
98 }
99 treeEntries[i] = entry
100 }
101
102 var parentPtr *string
103 if path != "" {
104 parentPtr = &path
105 }
106
107 var dotdotPtr *string
108 if path != "" {
109 dotdot := filepath.Dir(path)
110 if dotdot != "." {
111 dotdotPtr = &dotdot
112 }
113 }
114
115 // find the most recent commit across all entries for the directory-level last commit
116 var lastCommitInfo *types.LastCommitInfo
117 for _, file := range files {
118 if file.LastCommit == nil {
119 continue
120 }
121 if lastCommitInfo == nil {
122 lastCommitInfo = file.LastCommit
123 continue
124 }
125 if file.LastCommit.When.After(lastCommitInfo.When) {
126 lastCommitInfo = file.LastCommit
127 }
128 }
129
130 var lastCommit *tangled.GitTempGetTree_LastCommit
131 if lastCommitInfo != nil {
132 lastCommit = &tangled.GitTempGetTree_LastCommit{
133 Hash: lastCommitInfo.Hash.String(),
134 Message: lastCommitInfo.Message,
135 When: lastCommitInfo.When.Format(time.RFC3339),
136 }
137 if commit, err := gr.Commit(lastCommitInfo.Hash); err == nil {
138 lastCommit.Author = &tangled.GitTempGetTree_Signature{
139 Name: commit.Author.Name,
140 Email: commit.Author.Email,
141 }
142 }
143 }
144
145 return &tangled.GitTempGetTree_Output{
146 Ref: ref,
147 Parent: parentPtr,
148 Dotdot: dotdotPtr,
149 Files: treeEntries,
150 LastCommit: lastCommit,
151 Readme: &tangled.GitTempGetTree_Readme{
152 Filename: readmeFileName,
153 Contents: readmeContents,
154 },
155 }, nil
156}