Monorepo for Tangled
tangled.org
1package repo
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 "strconv"
10
11 "tangled.org/core/api/tangled"
12 "tangled.org/core/appview/commitverify"
13 "tangled.org/core/appview/db"
14 "tangled.org/core/appview/pages"
15 "tangled.org/core/types"
16 xrpcclient "tangled.org/core/xrpc/xrpcclient"
17
18 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
19 "github.com/go-chi/chi/v5"
20 "github.com/go-git/go-git/v5/plumbing"
21)
22
23func (rp *Repo) CommitRawDiff(w http.ResponseWriter, r *http.Request) {
24 rp.serveRawCommit(w, r, "diff")
25}
26
27func (rp *Repo) CommitRawPatch(w http.ResponseWriter, r *http.Request) {
28 rp.serveRawCommit(w, r, "patch")
29}
30
31func (rp *Repo) serveRawCommit(w http.ResponseWriter, r *http.Request, format string) {
32 l := rp.logger.With("handler", "CommitRaw", "format", format)
33
34 f, err := rp.repoResolver.Resolve(r)
35 if err != nil {
36 l.Error("failed to resolve repo", "err", err)
37 return
38 }
39
40 ref := chi.URLParam(r, "ref")
41 ref, _ = url.PathUnescape(ref)
42
43 if !plumbing.IsHash(ref) {
44 rp.pages.Error404(w)
45 return
46 }
47
48 scheme := "http"
49 if !rp.config.Core.Dev {
50 scheme = "https"
51 }
52
53 xrpcc := &indigoxrpc.Client{
54 Host: fmt.Sprintf("%s://%s", scheme, f.Knot),
55 }
56
57 xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, f.RepoIdentifier())
58 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
59 l.Error("failed to call XRPC repo.diff", "xrpcerr", xrpcerr, "err", err)
60 rp.pages.Error503(w)
61 return
62 }
63
64 var result types.RepoCommitResponse
65 if err := json.Unmarshal(xrpcBytes, &result); err != nil {
66 l.Error("failed to decode XRPC response", "err", err)
67 rp.pages.Error503(w)
68 return
69 }
70
71 filename := ref[:7] + "." + format
72 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
73 w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", filename))
74
75 switch format {
76 case "patch":
77 io.WriteString(w, renderFormatPatch(result.Diff))
78 default:
79 io.WriteString(w, renderUnifiedDiff(result.Diff))
80 }
81}
82
83func (rp *Repo) Log(w http.ResponseWriter, r *http.Request) {
84 l := rp.logger.With("handler", "RepoLog")
85
86 f, err := rp.repoResolver.Resolve(r)
87 if err != nil {
88 l.Error("failed to fully resolve repo", "err", err)
89 return
90 }
91
92 page := 1
93 if r.URL.Query().Get("page") != "" {
94 page, err = strconv.Atoi(r.URL.Query().Get("page"))
95 if err != nil {
96 page = 1
97 }
98 }
99
100 ref := chi.URLParam(r, "ref")
101 ref, _ = url.PathUnescape(ref)
102
103 xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url}
104
105 limit := int64(60)
106 cursor := ""
107 if page > 1 {
108 // Convert page number to cursor (offset)
109 offset := (page - 1) * int(limit)
110 cursor = strconv.Itoa(offset)
111 }
112
113 xrpcBytes, err := tangled.GitTempListCommits(r.Context(), xrpcc, cursor, limit, ref, f.RepoDid)
114 if err != nil {
115 l.Error("failed to call XRPC repo.log", "err", err)
116 rp.pages.Error503(w)
117 return
118 }
119
120 var xrpcResp types.RepoLogResponse
121 if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil {
122 l.Error("failed to decode XRPC response", "err", err)
123 rp.pages.Error503(w)
124 return
125 }
126
127 tagBytes, err := tangled.GitTempListTags(r.Context(), xrpcc, "", 0, f.RepoDid)
128 if err != nil {
129 l.Error("failed to call XRPC repo.tags", "err", err)
130 rp.pages.Error503(w)
131 return
132 }
133
134 tagMap := make(map[string][]string)
135 if tagBytes != nil {
136 var tagResp types.RepoTagsResponse
137 if err := json.Unmarshal(tagBytes, &tagResp); err == nil {
138 for _, tag := range tagResp.Tags {
139 hash := tag.Hash
140 if tag.Tag != nil {
141 hash = tag.Tag.Target.String()
142 }
143 tagMap[hash] = append(tagMap[hash], tag.Name)
144 }
145 }
146 }
147
148 branchBytes, err := tangled.GitTempListBranches(r.Context(), xrpcc, "", 0, f.RepoDid)
149 if err != nil {
150 l.Error("failed to call XRPC repo.branches", "err", err)
151 rp.pages.Error503(w)
152 return
153 }
154
155 if branchBytes != nil {
156 var branchResp types.RepoBranchesResponse
157 if err := json.Unmarshal(branchBytes, &branchResp); err == nil {
158 for _, branch := range branchResp.Branches {
159 tagMap[branch.Hash] = append(tagMap[branch.Hash], branch.Name)
160 }
161 }
162 }
163
164 user := rp.oauth.GetMultiAccountUser(r)
165
166 emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true)
167 if err != nil {
168 l.Error("failed to fetch email to did mapping", "err", err)
169 }
170
171 vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, xrpcResp.Commits)
172 if err != nil {
173 l.Error("failed to GetVerifiedObjectCommits", "err", err)
174 }
175
176 var shas []string
177 for _, c := range xrpcResp.Commits {
178 shas = append(shas, c.Hash.String())
179 }
180 pipelines, err := getPipelineStatuses(r.Context(), f, shas)
181 if err != nil {
182 l.Error("failed to getPipelineStatuses", "err", err)
183 // non-fatal
184 }
185
186 rp.pages.RepoLog(w, pages.RepoLogParams{
187 BaseParams: pages.BaseParamsFromContext(r.Context()),
188 TagMap: tagMap,
189 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
190 RepoLogResponse: xrpcResp,
191 EmailToDid: emailToDidMap,
192 VerifiedCommits: vc,
193 Pipelines: pipelines,
194 })
195}
196
197func (rp *Repo) Commit(w http.ResponseWriter, r *http.Request) {
198 l := rp.logger.With("handler", "RepoCommit")
199
200 f, err := rp.repoResolver.Resolve(r)
201 if err != nil {
202 l.Error("failed to fully resolve repo", "err", err)
203 return
204 }
205 ref := chi.URLParam(r, "ref")
206 ref, _ = url.PathUnescape(ref)
207
208 var diffOpts types.DiffOpts
209 if d := r.URL.Query().Get("diff"); d == "split" {
210 diffOpts.Split = true
211 }
212
213 if !plumbing.IsHash(ref) {
214 rp.pages.Error404(w)
215 return
216 }
217
218 scheme := "http"
219 if !rp.config.Core.Dev {
220 scheme = "https"
221 }
222 host := fmt.Sprintf("%s://%s", scheme, f.Knot)
223 xrpcc := &indigoxrpc.Client{
224 Host: host,
225 }
226
227 xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, f.RepoIdentifier())
228 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
229 l.Error("failed to call XRPC repo.diff", "xrpcerr", xrpcerr, "err", err)
230 rp.pages.Error503(w)
231 return
232 }
233
234 var result types.RepoCommitResponse
235 if err := json.Unmarshal(xrpcBytes, &result); err != nil {
236 l.Error("failed to decode XRPC response", "err", err)
237 rp.pages.Error503(w)
238 return
239 }
240
241 emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Committer.Email, result.Diff.Commit.Author.Email}, true)
242 if err != nil {
243 l.Error("failed to get email to did mapping", "err", err)
244 }
245
246 vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.Commit{result.Diff.Commit})
247 if err != nil {
248 l.Error("failed to GetVerifiedCommits", "err", err)
249 }
250
251 user := rp.oauth.GetMultiAccountUser(r)
252 pipelines, err := getPipelineStatuses(r.Context(), f, []string{result.Diff.Commit.This})
253 if err != nil {
254 l.Error("failed to getPipelineStatuses", "err", err)
255 // non-fatal
256 }
257 var pipeline *types.Pipeline
258 if p, ok := pipelines[result.Diff.Commit.This]; ok {
259 pipeline = &p
260 }
261
262 rp.pages.RepoCommit(w, pages.RepoCommitParams{
263 BaseParams: pages.BaseParamsFromContext(r.Context()),
264 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
265 RepoCommitResponse: result,
266 EmailToDid: emailToDidMap,
267 VerifiedCommit: vc,
268 Pipeline: pipeline,
269 DiffOpts: diffOpts,
270 })
271}