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