Monorepo for Tangled
1package xrpc
2
3import (
4 "net/http"
5 "path/filepath"
6 "time"
7 "unicode/utf8"
8
9 "tangled.org/core/api/tangled"
10 "tangled.org/core/appview/pages/markup"
11 "tangled.org/core/knotserver/git"
12 "tangled.org/core/types"
13 xrpcerr "tangled.org/core/xrpc/errors"
14)
15
16func (x *Xrpc) RepoTree(w http.ResponseWriter, r *http.Request) {
17 ctx := r.Context()
18
19 repo := r.URL.Query().Get("repo")
20 repoPath, err := x.parseRepoParam(repo)
21 if err != nil {
22 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
23 return
24 }
25
26 ref := r.URL.Query().Get("ref")
27 // ref can be empty (git.Open handles this)
28
29 path := r.URL.Query().Get("path")
30 // path can be empty (defaults to root)
31
32 gr, err := git.Open(repoPath, ref)
33 if err != nil {
34 x.Logger.Error("failed to open git repository", "error", err, "path", repoPath, "ref", ref)
35 writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
36 return
37 }
38
39 files, err := gr.FileTree(ctx, path)
40 if err != nil {
41 x.Logger.Error("failed to get file tree", "error", err, "path", path)
42 writeError(w, xrpcerr.NewXrpcError(
43 xrpcerr.WithTag("PathNotFound"),
44 xrpcerr.WithMessage("failed to read repository tree"),
45 ), http.StatusNotFound)
46 return
47 }
48
49 // if any of these files are a readme candidate, pass along its blob contents too
50 var readmeFileName string
51 var readmeContents string
52 for _, file := range files {
53 if markup.IsReadmeFile(file.Name) {
54 contents, err := gr.RawContent(filepath.Join(path, file.Name))
55 if err != nil {
56 x.Logger.Error("failed to read contents of file", "path", path, "file", file.Name)
57 }
58
59 if utf8.Valid(contents) {
60 readmeFileName = file.Name
61 readmeContents = string(contents)
62 break
63 }
64 }
65 }
66
67 // convert NiceTree -> tangled.RepoTree_TreeEntry
68 treeEntries := make([]*tangled.RepoTree_TreeEntry, len(files))
69 for i, file := range files {
70 entry := &tangled.RepoTree_TreeEntry{
71 Name: file.Name,
72 Mode: file.Mode,
73 Size: file.Size,
74 }
75
76 if file.LastCommit != nil {
77 entry.Last_commit = &tangled.RepoTree_LastCommit{
78 Hash: file.LastCommit.Hash.String(),
79 Message: file.LastCommit.Message,
80 When: file.LastCommit.When.Format(time.RFC3339),
81 }
82 }
83
84 treeEntries[i] = entry
85 }
86
87 var parentPtr *string
88 if path != "" {
89 parentPtr = &path
90 }
91
92 var dotdotPtr *string
93 if path != "" {
94 dotdot := filepath.Dir(path)
95 if dotdot != "." {
96 dotdotPtr = &dotdot
97 }
98 }
99
100 response := tangled.RepoTree_Output{
101 Ref: ref,
102 Parent: parentPtr,
103 Dotdot: dotdotPtr,
104 Files: treeEntries,
105 Readme: &tangled.RepoTree_Readme{
106 Filename: readmeFileName,
107 Contents: readmeContents,
108 },
109 }
110
111 // calculate lastCommit for the directory as a whole
112 var lastCommitTree *types.LastCommitInfo
113 for _, e := range files {
114 if e.LastCommit == nil {
115 continue
116 }
117
118 if lastCommitTree == nil {
119 lastCommitTree = e.LastCommit
120 continue
121 }
122
123 if lastCommitTree.When.After(e.LastCommit.When) {
124 lastCommitTree = e.LastCommit
125 }
126 }
127
128 if lastCommitTree != nil {
129 response.LastCommit = &tangled.RepoTree_LastCommit{
130 Hash: lastCommitTree.Hash.String(),
131 Message: lastCommitTree.Message,
132 When: lastCommitTree.When.Format(time.RFC3339),
133 }
134
135 // try to get author information
136 commit, err := gr.Commit(lastCommitTree.Hash)
137 if err == nil {
138 response.LastCommit.Author = &tangled.RepoTree_Signature{
139 Name: commit.Author.Name,
140 Email: commit.Author.Email,
141 }
142 }
143 }
144
145 writeJson(w, response)
146}