Monorepo for Tangled tangled.org

appview/repo: rework repo handlers to use xrpc calls

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

authored by anirudh.fi and committed by oppi.li f25a51e4 f21c584e

Changed files
+629 -179
appview
pages
markup
templates
repo
fragments
repo
xrpcclient
+12
appview/pages/markup/format.go
··· 13 13 FormatMarkdown: []string{".md", ".markdown", ".mdown", ".mkdn", ".mkd"}, 14 14 } 15 15 16 + // ReadmeFilenames contains the list of common README filenames to search for, 17 + // in order of preference. Only includes well-supported formats. 18 + var ReadmeFilenames = []string{ 19 + "README.md", "readme.md", 20 + "README", 21 + "readme", 22 + "README.markdown", 23 + "readme.markdown", 24 + "README.txt", 25 + "readme.txt", 26 + } 27 + 16 28 func GetFormat(filename string) Format { 17 29 for format, extensions := range FileTypes { 18 30 for _, extension := range extensions {
+6 -1
appview/pages/pages.go
··· 763 763 ShowRendered bool 764 764 RenderToggle bool 765 765 RenderedContents template.HTML 766 - types.RepoBlobResponse 766 + *tangled.RepoBlob_Output 767 + // Computed fields for template compatibility 768 + Contents string 769 + Lines int 770 + SizeHint uint64 771 + IsBinary bool 767 772 } 768 773 769 774 func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
+6
appview/pages/templates/repo/fragments/diff.html
··· 11 11 {{ $last := sub (len $diff) 1 }} 12 12 13 13 <div class="flex flex-col gap-4"> 14 + {{ if eq (len $diff) 0 }} 15 + <div class="text-center text-gray-500 dark:text-gray-400 py-8"> 16 + <p>No differences found between the selected revisions.</p> 17 + </div> 18 + {{ else }} 14 19 {{ range $idx, $hunk := $diff }} 15 20 {{ with $hunk }} 16 21 <details open id="file-{{ .Name.New }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}"> ··· 49 54 </div> 50 55 </details> 51 56 {{ end }} 57 + {{ end }} 52 58 {{ end }} 53 59 </div> 54 60 {{ end }}
+26 -8
appview/repo/artifact.go
··· 1 1 package repo 2 2 3 3 import ( 4 + "context" 5 + "encoding/json" 4 6 "fmt" 5 7 "log" 6 8 "net/http" ··· 9 11 10 12 comatproto "github.com/bluesky-social/indigo/api/atproto" 11 13 lexutil "github.com/bluesky-social/indigo/lex/util" 14 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 12 15 "github.com/dustin/go-humanize" 13 16 "github.com/go-chi/chi/v5" 14 17 "github.com/go-git/go-git/v5/plumbing" ··· 17 20 "tangled.sh/tangled.sh/core/appview/db" 18 21 "tangled.sh/tangled.sh/core/appview/pages" 19 22 "tangled.sh/tangled.sh/core/appview/reporesolver" 20 - "tangled.sh/tangled.sh/core/knotclient" 23 + "tangled.sh/tangled.sh/core/appview/xrpcclient" 21 24 "tangled.sh/tangled.sh/core/tid" 22 25 "tangled.sh/tangled.sh/core/types" 23 26 ) ··· 33 36 return 34 37 } 35 38 36 - tag, err := rp.resolveTag(f, tagParam) 39 + tag, err := rp.resolveTag(r.Context(), f, tagParam) 37 40 if err != nil { 38 41 log.Println("failed to resolve tag", err) 39 42 rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution") ··· 140 143 return 141 144 } 142 145 143 - tag, err := rp.resolveTag(f, tagParam) 146 + tag, err := rp.resolveTag(r.Context(), f, tagParam) 144 147 if err != nil { 145 148 log.Println("failed to resolve tag", err) 146 149 rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution") ··· 259 262 w.Write([]byte{}) 260 263 } 261 264 262 - func (rp *Repo) resolveTag(f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) { 265 + func (rp *Repo) resolveTag(ctx context.Context, f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) { 263 266 tagParam, err := url.QueryUnescape(tagParam) 264 267 if err != nil { 265 268 return nil, err 266 269 } 267 270 268 - us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 269 - if err != nil { 270 - return nil, err 271 + scheme := "http" 272 + if !rp.config.Core.Dev { 273 + scheme = "https" 274 + } 275 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 276 + xrpcc := &indigoxrpc.Client{ 277 + Host: host, 271 278 } 272 279 273 - result, err := us.Tags(f.OwnerDid(), f.Name) 280 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 281 + xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo) 274 282 if err != nil { 283 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 284 + log.Println("failed to call XRPC repo.tags", xrpcerr) 285 + return nil, xrpcerr 286 + } 275 287 log.Println("failed to reach knotserver", err) 288 + return nil, err 289 + } 290 + 291 + var result types.RepoTagsResponse 292 + if err := json.Unmarshal(xrpcBytes, &result); err != nil { 293 + log.Println("failed to decode XRPC tags response", err) 276 294 return nil, err 277 295 } 278 296
+220 -16
appview/repo/index.go
··· 1 1 package repo 2 2 3 3 import ( 4 + "fmt" 4 5 "log" 5 6 "net/http" 6 7 "slices" 7 8 "sort" 8 9 "strings" 10 + "sync" 11 + "time" 9 12 13 + "context" 14 + "encoding/json" 15 + 16 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 17 + "github.com/go-git/go-git/v5/plumbing" 18 + "tangled.sh/tangled.sh/core/api/tangled" 10 19 "tangled.sh/tangled.sh/core/appview/commitverify" 11 20 "tangled.sh/tangled.sh/core/appview/db" 12 21 "tangled.sh/tangled.sh/core/appview/pages" 22 + "tangled.sh/tangled.sh/core/appview/pages/markup" 13 23 "tangled.sh/tangled.sh/core/appview/reporesolver" 14 - "tangled.sh/tangled.sh/core/knotclient" 24 + "tangled.sh/tangled.sh/core/appview/xrpcclient" 15 25 "tangled.sh/tangled.sh/core/types" 16 26 17 27 "github.com/go-chi/chi/v5" ··· 27 37 return 28 38 } 29 39 30 - us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 31 - if err != nil { 32 - log.Printf("failed to create unsigned client for %s", f.Knot) 33 - rp.pages.Error503(w) 34 - return 40 + scheme := "http" 41 + if !rp.config.Core.Dev { 42 + scheme = "https" 43 + } 44 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 45 + xrpcc := &indigoxrpc.Client{ 46 + Host: host, 35 47 } 36 48 37 - result, err := us.Index(f.OwnerDid(), f.Name, ref) 49 + // Build index response from multiple XRPC calls 50 + result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref) 38 51 if err != nil { 39 52 rp.pages.Error503(w) 40 - log.Println("failed to reach knotserver", err) 53 + log.Println("failed to build index response", err) 41 54 return 42 55 } 43 56 ··· 102 115 repoInfo := f.RepoInfo(user) 103 116 104 117 // TODO: a bit dirty 105 - languageInfo, err := rp.getLanguageInfo(f, us, result.Ref, ref == "") 118 + languageInfo, err := rp.getLanguageInfo(r.Context(), f, xrpcc, result.Ref, ref == "") 106 119 if err != nil { 107 120 log.Printf("failed to compute language percentages: %s", err) 108 121 // non-fatal ··· 135 148 } 136 149 137 150 func (rp *Repo) getLanguageInfo( 151 + ctx context.Context, 138 152 f *reporesolver.ResolvedRepo, 139 - us *knotclient.UnsignedClient, 153 + xrpcc *indigoxrpc.Client, 140 154 currentRef string, 141 155 isDefaultRef bool, 142 156 ) ([]types.RepoLanguageDetails, error) { ··· 148 162 ) 149 163 150 164 if err != nil || langs == nil { 151 - // non-fatal, fetch langs from ks 152 - ls, err := us.RepoLanguages(f.OwnerDid(), f.Name, currentRef) 165 + // non-fatal, fetch langs from ks via XRPC 166 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 167 + ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, repo) 153 168 if err != nil { 169 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 170 + log.Println("failed to call XRPC repo.languages", xrpcerr) 171 + return nil, xrpcerr 172 + } 154 173 return nil, err 155 174 } 156 - if ls == nil { 175 + 176 + if ls == nil || ls.Languages == nil { 157 177 return nil, nil 158 178 } 159 179 160 - for l, s := range ls.Languages { 180 + for _, lang := range ls.Languages { 161 181 langs = append(langs, db.RepoLanguage{ 162 182 RepoAt: f.RepoAt(), 163 183 Ref: currentRef, 164 184 IsDefaultRef: isDefaultRef, 165 - Language: l, 166 - Bytes: s, 185 + Language: lang.Name, 186 + Bytes: lang.Size, 167 187 }) 168 188 } 169 189 ··· 206 226 207 227 return languageStats, nil 208 228 } 229 + 230 + // buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel 231 + func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client, f *reporesolver.ResolvedRepo, ref string) (*types.RepoIndexResponse, error) { 232 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 233 + 234 + // first get branches to determine the ref if not specified 235 + branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo) 236 + if err != nil { 237 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 238 + log.Println("failed to call XRPC repo.branches", xrpcerr) 239 + return nil, xrpcerr 240 + } 241 + return nil, err 242 + } 243 + 244 + var branchesResp types.RepoBranchesResponse 245 + if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil { 246 + return nil, err 247 + } 248 + 249 + // if no ref specified, use default branch or first available 250 + if ref == "" && len(branchesResp.Branches) > 0 { 251 + for _, branch := range branchesResp.Branches { 252 + if branch.IsDefault { 253 + ref = branch.Name 254 + break 255 + } 256 + } 257 + if ref == "" { 258 + ref = branchesResp.Branches[0].Name 259 + } 260 + } 261 + 262 + // check if repo is empty 263 + if len(branchesResp.Branches) == 0 { 264 + return &types.RepoIndexResponse{ 265 + IsEmpty: true, 266 + Branches: branchesResp.Branches, 267 + }, nil 268 + } 269 + 270 + // now run the remaining queries in parallel 271 + var wg sync.WaitGroup 272 + var mu sync.Mutex 273 + var errs []error 274 + 275 + var ( 276 + tagsResp types.RepoTagsResponse 277 + treeResp *tangled.RepoTree_Output 278 + logResp types.RepoLogResponse 279 + readmeContent string 280 + readmeFileName string 281 + ) 282 + 283 + // tags 284 + wg.Add(1) 285 + go func() { 286 + defer wg.Done() 287 + tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo) 288 + if err != nil { 289 + mu.Lock() 290 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 291 + log.Println("failed to call XRPC repo.tags", xrpcerr) 292 + errs = append(errs, xrpcerr) 293 + } else { 294 + errs = append(errs, err) 295 + } 296 + mu.Unlock() 297 + return 298 + } 299 + 300 + if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil { 301 + mu.Lock() 302 + errs = append(errs, err) 303 + mu.Unlock() 304 + } 305 + }() 306 + 307 + // tree/files 308 + wg.Add(1) 309 + go func() { 310 + defer wg.Done() 311 + resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo) 312 + if err != nil { 313 + mu.Lock() 314 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 315 + log.Println("failed to call XRPC repo.tree", xrpcerr) 316 + errs = append(errs, xrpcerr) 317 + } else { 318 + errs = append(errs, err) 319 + } 320 + mu.Unlock() 321 + return 322 + } 323 + treeResp = resp 324 + }() 325 + 326 + // commits 327 + wg.Add(1) 328 + go func() { 329 + defer wg.Done() 330 + logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo) 331 + if err != nil { 332 + mu.Lock() 333 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 334 + log.Println("failed to call XRPC repo.log", xrpcerr) 335 + errs = append(errs, xrpcerr) 336 + } else { 337 + errs = append(errs, err) 338 + } 339 + mu.Unlock() 340 + return 341 + } 342 + 343 + if err := json.Unmarshal(logBytes, &logResp); err != nil { 344 + mu.Lock() 345 + errs = append(errs, err) 346 + mu.Unlock() 347 + } 348 + }() 349 + 350 + // readme content 351 + wg.Add(1) 352 + go func() { 353 + defer wg.Done() 354 + for _, filename := range markup.ReadmeFilenames { 355 + blobResp, err := tangled.RepoBlob(ctx, xrpcc, filename, false, ref, repo) 356 + if err != nil { 357 + continue 358 + } 359 + 360 + if blobResp == nil { 361 + continue 362 + } 363 + 364 + readmeContent = blobResp.Content 365 + readmeFileName = filename 366 + break 367 + } 368 + }() 369 + 370 + wg.Wait() 371 + 372 + if len(errs) > 0 { 373 + return nil, errs[0] // return first error 374 + } 375 + 376 + var files []types.NiceTree 377 + if treeResp != nil && treeResp.Files != nil { 378 + for _, file := range treeResp.Files { 379 + niceFile := types.NiceTree{ 380 + IsFile: file.Is_file, 381 + IsSubtree: file.Is_subtree, 382 + Name: file.Name, 383 + Mode: file.Mode, 384 + Size: file.Size, 385 + } 386 + if file.Last_commit != nil { 387 + when, _ := time.Parse(time.RFC3339, file.Last_commit.When) 388 + niceFile.LastCommit = &types.LastCommitInfo{ 389 + Hash: plumbing.NewHash(file.Last_commit.Hash), 390 + Message: file.Last_commit.Message, 391 + When: when, 392 + } 393 + } 394 + files = append(files, niceFile) 395 + } 396 + } 397 + 398 + result := &types.RepoIndexResponse{ 399 + IsEmpty: false, 400 + Ref: ref, 401 + Readme: readmeContent, 402 + ReadmeFileName: readmeFileName, 403 + Commits: logResp.Commits, 404 + Description: logResp.Description, 405 + Files: files, 406 + Branches: branchesResp.Branches, 407 + Tags: tagsResp.Tags, 408 + TotalCommits: logResp.Total, 409 + } 410 + 411 + return result, nil 412 + }
+358 -153
appview/repo/repo.go
··· 11 11 "log/slog" 12 12 "net/http" 13 13 "net/url" 14 + "path" 14 15 "path/filepath" 15 16 "slices" 16 17 "strconv" ··· 19 20 20 21 comatproto "github.com/bluesky-social/indigo/api/atproto" 21 22 lexutil "github.com/bluesky-social/indigo/lex/util" 23 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 22 24 "tangled.sh/tangled.sh/core/api/tangled" 23 25 "tangled.sh/tangled.sh/core/appview/commitverify" 24 26 "tangled.sh/tangled.sh/core/appview/config" ··· 31 33 xrpcclient "tangled.sh/tangled.sh/core/appview/xrpcclient" 32 34 "tangled.sh/tangled.sh/core/eventconsumer" 33 35 "tangled.sh/tangled.sh/core/idresolver" 34 - "tangled.sh/tangled.sh/core/knotclient" 35 36 "tangled.sh/tangled.sh/core/patchutil" 36 37 "tangled.sh/tangled.sh/core/rbac" 37 38 "tangled.sh/tangled.sh/core/tid" ··· 92 93 return 93 94 } 94 95 95 - var uri string 96 - if rp.config.Core.Dev { 97 - uri = "http" 98 - } else { 99 - uri = "https" 96 + scheme := "http" 97 + if !rp.config.Core.Dev { 98 + scheme = "https" 99 + } 100 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 101 + xrpcc := &indigoxrpc.Client{ 102 + Host: host, 103 + } 104 + 105 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 106 + archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", refParam, repo) 107 + if err != nil { 108 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 109 + log.Println("failed to call XRPC repo.archive", xrpcerr) 110 + rp.pages.Error503(w) 111 + return 112 + } 113 + rp.pages.Error404(w) 114 + return 100 115 } 101 - url := fmt.Sprintf("%s://%s/%s/%s/archive/%s.tar.gz", uri, f.Knot, f.OwnerDid(), f.Name, url.PathEscape(refParam)) 102 116 103 - http.Redirect(w, r, url, http.StatusFound) 117 + // Set headers for file download 118 + filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, refParam) 119 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 120 + w.Header().Set("Content-Type", "application/gzip") 121 + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) 122 + 123 + // Write the archive data directly 124 + w.Write(archiveBytes) 104 125 } 105 126 106 127 func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) { ··· 120 141 121 142 ref := chi.URLParam(r, "ref") 122 143 123 - us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 144 + scheme := "http" 145 + if !rp.config.Core.Dev { 146 + scheme = "https" 147 + } 148 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 149 + xrpcc := &indigoxrpc.Client{ 150 + Host: host, 151 + } 152 + 153 + limit := int64(60) 154 + cursor := "" 155 + if page > 1 { 156 + // Convert page number to cursor (offset) 157 + offset := (page - 1) * int(limit) 158 + cursor = strconv.Itoa(offset) 159 + } 160 + 161 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 162 + xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo) 124 163 if err != nil { 125 - log.Println("failed to create unsigned client", err) 164 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 165 + log.Println("failed to call XRPC repo.log", xrpcerr) 166 + rp.pages.Error503(w) 167 + return 168 + } 169 + rp.pages.Error404(w) 126 170 return 127 171 } 128 172 129 - repolog, err := us.Log(f.OwnerDid(), f.Name, ref, page) 130 - if err != nil { 173 + var xrpcResp types.RepoLogResponse 174 + if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil { 175 + log.Println("failed to decode XRPC response", err) 131 176 rp.pages.Error503(w) 132 - log.Println("failed to reach knotserver", err) 133 177 return 134 178 } 135 179 136 - tagResult, err := us.Tags(f.OwnerDid(), f.Name) 180 + tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 137 181 if err != nil { 138 - rp.pages.Error503(w) 139 - log.Println("failed to reach knotserver", err) 140 - return 182 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 183 + log.Println("failed to call XRPC repo.tags", xrpcerr) 184 + rp.pages.Error503(w) 185 + return 186 + } 141 187 } 142 188 143 189 tagMap := make(map[string][]string) 144 - for _, tag := range tagResult.Tags { 145 - hash := tag.Hash 146 - if tag.Tag != nil { 147 - hash = tag.Tag.Target.String() 190 + if tagBytes != nil { 191 + var tagResp types.RepoTagsResponse 192 + if err := json.Unmarshal(tagBytes, &tagResp); err == nil { 193 + for _, tag := range tagResp.Tags { 194 + tagMap[tag.Hash] = append(tagMap[tag.Hash], tag.Name) 195 + } 148 196 } 149 - tagMap[hash] = append(tagMap[hash], tag.Name) 150 197 } 151 198 152 - branchResult, err := us.Branches(f.OwnerDid(), f.Name) 199 + branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 153 200 if err != nil { 154 - rp.pages.Error503(w) 155 - log.Println("failed to reach knotserver", err) 156 - return 201 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 202 + log.Println("failed to call XRPC repo.branches", xrpcerr) 203 + rp.pages.Error503(w) 204 + return 205 + } 157 206 } 158 207 159 - for _, branch := range branchResult.Branches { 160 - hash := branch.Hash 161 - tagMap[hash] = append(tagMap[hash], branch.Name) 208 + if branchBytes != nil { 209 + var branchResp types.RepoBranchesResponse 210 + if err := json.Unmarshal(branchBytes, &branchResp); err == nil { 211 + for _, branch := range branchResp.Branches { 212 + tagMap[branch.Hash] = append(tagMap[branch.Hash], branch.Name) 213 + } 214 + } 162 215 } 163 216 164 217 user := rp.oauth.GetUser(r) 165 218 166 - emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(repolog.Commits), true) 219 + emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true) 167 220 if err != nil { 168 221 log.Println("failed to fetch email to did mapping", err) 169 222 } 170 223 171 - vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, repolog.Commits) 224 + vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits) 172 225 if err != nil { 173 226 log.Println(err) 174 227 } ··· 176 229 repoInfo := f.RepoInfo(user) 177 230 178 231 var shas []string 179 - for _, c := range repolog.Commits { 232 + for _, c := range xrpcResp.Commits { 180 233 shas = append(shas, c.Hash.String()) 181 234 } 182 235 pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas) ··· 189 242 LoggedInUser: user, 190 243 TagMap: tagMap, 191 244 RepoInfo: repoInfo, 192 - RepoLogResponse: *repolog, 245 + RepoLogResponse: xrpcResp, 193 246 EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap), 194 247 VerifiedCommits: vc, 195 248 Pipelines: pipelines, ··· 301 354 return 302 355 } 303 356 ref := chi.URLParam(r, "ref") 304 - protocol := "http" 305 - if !rp.config.Core.Dev { 306 - protocol = "https" 307 - } 308 357 309 358 var diffOpts types.DiffOpts 310 359 if d := r.URL.Query().Get("diff"); d == "split" { ··· 316 365 return 317 366 } 318 367 319 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref)) 320 - if err != nil { 321 - rp.pages.Error503(w) 322 - log.Println("failed to reach knotserver", err) 323 - return 368 + scheme := "http" 369 + if !rp.config.Core.Dev { 370 + scheme = "https" 371 + } 372 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 373 + xrpcc := &indigoxrpc.Client{ 374 + Host: host, 324 375 } 325 376 326 - body, err := io.ReadAll(resp.Body) 377 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 378 + xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo) 327 379 if err != nil { 328 - log.Printf("Error reading response body: %v", err) 380 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 381 + log.Println("failed to call XRPC repo.diff", xrpcerr) 382 + rp.pages.Error503(w) 383 + return 384 + } 385 + rp.pages.Error404(w) 329 386 return 330 387 } 331 388 332 389 var result types.RepoCommitResponse 333 - err = json.Unmarshal(body, &result) 334 - if err != nil { 335 - log.Println("failed to parse response:", err) 390 + if err := json.Unmarshal(xrpcBytes, &result); err != nil { 391 + log.Println("failed to decode XRPC response", err) 392 + rp.pages.Error503(w) 336 393 return 337 394 } 338 395 ··· 378 435 379 436 ref := chi.URLParam(r, "ref") 380 437 treePath := chi.URLParam(r, "*") 381 - protocol := "http" 382 - if !rp.config.Core.Dev { 383 - protocol = "https" 384 - } 385 438 386 439 // if the tree path has a trailing slash, let's strip it 387 440 // so we don't 404 388 441 treePath = strings.TrimSuffix(treePath, "/") 389 442 390 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, treePath)) 391 - if err != nil { 392 - rp.pages.Error503(w) 393 - log.Println("failed to reach knotserver", err) 394 - return 443 + scheme := "http" 444 + if !rp.config.Core.Dev { 445 + scheme = "https" 446 + } 447 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 448 + xrpcc := &indigoxrpc.Client{ 449 + Host: host, 395 450 } 396 451 397 - // uhhh so knotserver returns a 500 if the entry isn't found in 398 - // the requested tree path, so let's stick to not-OK here. 399 - // we can fix this once we build out the xrpc apis for these operations. 400 - if resp.StatusCode != http.StatusOK { 452 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 453 + xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo) 454 + if err != nil { 455 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 456 + log.Println("failed to call XRPC repo.tree", xrpcerr) 457 + rp.pages.Error503(w) 458 + return 459 + } 401 460 rp.pages.Error404(w) 402 461 return 403 462 } 404 463 405 - body, err := io.ReadAll(resp.Body) 406 - if err != nil { 407 - log.Printf("Error reading response body: %v", err) 408 - return 464 + // Convert XRPC response to internal types.RepoTreeResponse 465 + files := make([]types.NiceTree, len(xrpcResp.Files)) 466 + for i, xrpcFile := range xrpcResp.Files { 467 + file := types.NiceTree{ 468 + Name: xrpcFile.Name, 469 + Mode: xrpcFile.Mode, 470 + Size: int64(xrpcFile.Size), 471 + IsFile: xrpcFile.Is_file, 472 + IsSubtree: xrpcFile.Is_subtree, 473 + } 474 + 475 + // Convert last commit info if present 476 + if xrpcFile.Last_commit != nil { 477 + commitWhen, _ := time.Parse(time.RFC3339, xrpcFile.Last_commit.When) 478 + file.LastCommit = &types.LastCommitInfo{ 479 + Hash: plumbing.NewHash(xrpcFile.Last_commit.Hash), 480 + Message: xrpcFile.Last_commit.Message, 481 + When: commitWhen, 482 + } 483 + } 484 + 485 + files[i] = file 409 486 } 410 487 411 - var result types.RepoTreeResponse 412 - err = json.Unmarshal(body, &result) 413 - if err != nil { 414 - log.Println("failed to parse response:", err) 415 - return 488 + result := types.RepoTreeResponse{ 489 + Ref: xrpcResp.Ref, 490 + Files: files, 491 + } 492 + 493 + if xrpcResp.Parent != nil { 494 + result.Parent = *xrpcResp.Parent 495 + } 496 + if xrpcResp.Dotdot != nil { 497 + result.DotDot = *xrpcResp.Dotdot 416 498 } 417 499 418 500 // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated, ··· 451 533 return 452 534 } 453 535 454 - us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 536 + scheme := "http" 537 + if !rp.config.Core.Dev { 538 + scheme = "https" 539 + } 540 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 541 + xrpcc := &indigoxrpc.Client{ 542 + Host: host, 543 + } 544 + 545 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 546 + xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 455 547 if err != nil { 456 - log.Println("failed to create unsigned client", err) 548 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 549 + log.Println("failed to call XRPC repo.tags", xrpcerr) 550 + rp.pages.Error503(w) 551 + return 552 + } 553 + rp.pages.Error404(w) 457 554 return 458 555 } 459 556 460 - result, err := us.Tags(f.OwnerDid(), f.Name) 461 - if err != nil { 557 + var result types.RepoTagsResponse 558 + if err := json.Unmarshal(xrpcBytes, &result); err != nil { 559 + log.Println("failed to decode XRPC response", err) 462 560 rp.pages.Error503(w) 463 - log.Println("failed to reach knotserver", err) 464 561 return 465 562 } 466 563 ··· 496 593 rp.pages.RepoTags(w, pages.RepoTagsParams{ 497 594 LoggedInUser: user, 498 595 RepoInfo: f.RepoInfo(user), 499 - RepoTagsResponse: *result, 596 + RepoTagsResponse: result, 500 597 ArtifactMap: artifactMap, 501 598 DanglingArtifacts: danglingArtifacts, 502 599 }) ··· 509 606 return 510 607 } 511 608 512 - us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 609 + scheme := "http" 610 + if !rp.config.Core.Dev { 611 + scheme = "https" 612 + } 613 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 614 + xrpcc := &indigoxrpc.Client{ 615 + Host: host, 616 + } 617 + 618 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 619 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 513 620 if err != nil { 514 - log.Println("failed to create unsigned client", err) 621 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 622 + log.Println("failed to call XRPC repo.branches", xrpcerr) 623 + rp.pages.Error503(w) 624 + return 625 + } 626 + rp.pages.Error404(w) 515 627 return 516 628 } 517 629 518 - result, err := us.Branches(f.OwnerDid(), f.Name) 519 - if err != nil { 630 + var result types.RepoBranchesResponse 631 + if err := json.Unmarshal(xrpcBytes, &result); err != nil { 632 + log.Println("failed to decode XRPC response", err) 520 633 rp.pages.Error503(w) 521 - log.Println("failed to reach knotserver", err) 522 634 return 523 635 } 524 636 ··· 528 640 rp.pages.RepoBranches(w, pages.RepoBranchesParams{ 529 641 LoggedInUser: user, 530 642 RepoInfo: f.RepoInfo(user), 531 - RepoBranchesResponse: *result, 643 + RepoBranchesResponse: result, 532 644 }) 533 645 } 534 646 ··· 541 653 542 654 ref := chi.URLParam(r, "ref") 543 655 filePath := chi.URLParam(r, "*") 544 - protocol := "http" 656 + 657 + scheme := "http" 545 658 if !rp.config.Core.Dev { 546 - protocol = "https" 547 - } 548 - resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath)) 549 - if err != nil { 550 - rp.pages.Error503(w) 551 - log.Println("failed to reach knotserver", err) 552 - return 659 + scheme = "https" 553 660 } 554 - 555 - if resp.StatusCode == http.StatusNotFound { 556 - rp.pages.Error404(w) 557 - return 661 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 662 + xrpcc := &indigoxrpc.Client{ 663 + Host: host, 558 664 } 559 665 560 - body, err := io.ReadAll(resp.Body) 666 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 667 + resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo) 561 668 if err != nil { 562 - log.Printf("Error reading response body: %v", err) 669 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 670 + log.Println("failed to call XRPC repo.blob", xrpcerr) 671 + rp.pages.Error503(w) 672 + return 673 + } 674 + rp.pages.Error404(w) 563 675 return 564 676 } 565 677 566 - var result types.RepoBlobResponse 567 - err = json.Unmarshal(body, &result) 568 - if err != nil { 569 - log.Println("failed to parse response:", err) 570 - return 571 - } 678 + // Use XRPC response directly instead of converting to internal types 572 679 573 680 var breadcrumbs [][]string 574 681 breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) ··· 581 688 showRendered := false 582 689 renderToggle := false 583 690 584 - if markup.GetFormat(result.Path) == markup.FormatMarkdown { 691 + if markup.GetFormat(resp.Path) == markup.FormatMarkdown { 585 692 renderToggle = true 586 693 showRendered = r.URL.Query().Get("code") != "true" 587 694 } ··· 591 698 var isVideo bool 592 699 var contentSrc string 593 700 594 - if result.IsBinary { 595 - ext := strings.ToLower(filepath.Ext(result.Path)) 701 + if resp.IsBinary != nil && *resp.IsBinary { 702 + ext := strings.ToLower(filepath.Ext(resp.Path)) 596 703 switch ext { 597 704 case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp": 598 705 isImage = true ··· 602 709 unsupported = true 603 710 } 604 711 605 - // fetch the actual binary content like in RepoBlobRaw 712 + // fetch the raw binary content using sh.tangled.repo.blob xrpc 713 + repoName := path.Join("%s/%s", f.OwnerDid(), f.Name) 714 + blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true", 715 + scheme, f.Knot, url.QueryEscape(repoName), url.QueryEscape(ref), url.QueryEscape(filePath)) 606 716 607 - blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Name, ref, filePath) 608 717 contentSrc = blobURL 609 718 if !rp.config.Core.Dev { 610 719 contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL) 611 720 } 612 721 } 613 722 723 + lines := 0 724 + if resp.IsBinary == nil || !*resp.IsBinary { 725 + lines = strings.Count(resp.Content, "\n") + 1 726 + } 727 + 728 + var sizeHint uint64 729 + if resp.Size != nil { 730 + sizeHint = uint64(*resp.Size) 731 + } else { 732 + sizeHint = uint64(len(resp.Content)) 733 + } 734 + 614 735 user := rp.oauth.GetUser(r) 736 + 737 + // Determine if content is binary (dereference pointer) 738 + isBinary := false 739 + if resp.IsBinary != nil { 740 + isBinary = *resp.IsBinary 741 + } 742 + 615 743 rp.pages.RepoBlob(w, pages.RepoBlobParams{ 616 - LoggedInUser: user, 617 - RepoInfo: f.RepoInfo(user), 618 - RepoBlobResponse: result, 619 - BreadCrumbs: breadcrumbs, 620 - ShowRendered: showRendered, 621 - RenderToggle: renderToggle, 622 - Unsupported: unsupported, 623 - IsImage: isImage, 624 - IsVideo: isVideo, 625 - ContentSrc: contentSrc, 744 + LoggedInUser: user, 745 + RepoInfo: f.RepoInfo(user), 746 + BreadCrumbs: breadcrumbs, 747 + ShowRendered: showRendered, 748 + RenderToggle: renderToggle, 749 + Unsupported: unsupported, 750 + IsImage: isImage, 751 + IsVideo: isVideo, 752 + ContentSrc: contentSrc, 753 + RepoBlob_Output: resp, 754 + Contents: resp.Content, 755 + Lines: lines, 756 + SizeHint: sizeHint, 757 + IsBinary: isBinary, 626 758 }) 627 759 } 628 760 ··· 637 769 ref := chi.URLParam(r, "ref") 638 770 filePath := chi.URLParam(r, "*") 639 771 640 - protocol := "http" 772 + scheme := "http" 641 773 if !rp.config.Core.Dev { 642 - protocol = "https" 774 + scheme = "https" 643 775 } 644 776 645 - blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath) 777 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 778 + blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true", 779 + scheme, f.Knot, url.QueryEscape(repo), url.QueryEscape(ref), url.QueryEscape(filePath)) 646 780 647 781 req, err := http.NewRequest("GET", blobURL, nil) 648 782 if err != nil { ··· 685 819 return 686 820 } 687 821 688 - // Safely serve content based on type 689 822 if strings.HasPrefix(contentType, "text/") || isTextualMimeType(contentType) { 690 - // Serve all textual content as text/plain for security 823 + // serve all textual content as text/plain 691 824 w.Header().Set("Content-Type", "text/plain; charset=utf-8") 692 825 w.Write(body) 693 826 } else if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") { 694 - // Serve images and videos with their original content type 827 + // serve images and videos with their original content type 695 828 w.Header().Set("Content-Type", contentType) 696 829 w.Write(body) 697 830 } else { 698 - // Block potentially dangerous content types 699 831 w.WriteHeader(http.StatusUnsupportedMediaType) 700 832 w.Write([]byte("unsupported content type")) 701 833 return ··· 716 848 "message/", 717 849 } 718 850 719 - for _, t := range textualTypes { 720 - if mimeType == t { 721 - return true 722 - } 723 - } 724 - return false 851 + return slices.Contains(textualTypes, mimeType) 725 852 } 726 853 727 854 // modify the spindle configured for this repo ··· 1227 1354 f, err := rp.repoResolver.Resolve(r) 1228 1355 user := rp.oauth.GetUser(r) 1229 1356 1230 - us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1357 + scheme := "http" 1358 + if !rp.config.Core.Dev { 1359 + scheme = "https" 1360 + } 1361 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 1362 + xrpcc := &indigoxrpc.Client{ 1363 + Host: host, 1364 + } 1365 + 1366 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1367 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1231 1368 if err != nil { 1232 - log.Println("failed to create unsigned client", err) 1369 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1370 + log.Println("failed to call XRPC repo.branches", xrpcerr) 1371 + rp.pages.Error503(w) 1372 + return 1373 + } 1374 + rp.pages.Error503(w) 1233 1375 return 1234 1376 } 1235 1377 1236 - result, err := us.Branches(f.OwnerDid(), f.Name) 1237 - if err != nil { 1378 + var result types.RepoBranchesResponse 1379 + if err := json.Unmarshal(xrpcBytes, &result); err != nil { 1380 + log.Println("failed to decode XRPC response", err) 1238 1381 rp.pages.Error503(w) 1239 - log.Println("failed to reach knotserver", err) 1240 1382 return 1241 1383 } 1242 1384 ··· 1607 1749 return 1608 1750 } 1609 1751 1610 - us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1752 + scheme := "http" 1753 + if !rp.config.Core.Dev { 1754 + scheme = "https" 1755 + } 1756 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 1757 + xrpcc := &indigoxrpc.Client{ 1758 + Host: host, 1759 + } 1760 + 1761 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1762 + branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1611 1763 if err != nil { 1612 - log.Printf("failed to create unsigned client for %s", f.Knot) 1613 - rp.pages.Error503(w) 1764 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1765 + log.Println("failed to call XRPC repo.branches", xrpcerr) 1766 + rp.pages.Error503(w) 1767 + return 1768 + } 1769 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1614 1770 return 1615 1771 } 1616 1772 1617 - result, err := us.Branches(f.OwnerDid(), f.Name) 1618 - if err != nil { 1773 + var branchResult types.RepoBranchesResponse 1774 + if err := json.Unmarshal(branchBytes, &branchResult); err != nil { 1775 + log.Println("failed to decode XRPC branches response", err) 1619 1776 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1620 - log.Println("failed to reach knotserver", err) 1621 1777 return 1622 1778 } 1623 - branches := result.Branches 1779 + branches := branchResult.Branches 1624 1780 1625 1781 sortBranches(branches) 1626 1782 ··· 1644 1800 head = queryHead 1645 1801 } 1646 1802 1647 - tags, err := us.Tags(f.OwnerDid(), f.Name) 1803 + tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 1648 1804 if err != nil { 1805 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1806 + log.Println("failed to call XRPC repo.tags", xrpcerr) 1807 + rp.pages.Error503(w) 1808 + return 1809 + } 1649 1810 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1650 - log.Println("failed to reach knotserver", err) 1811 + return 1812 + } 1813 + 1814 + var tags types.RepoTagsResponse 1815 + if err := json.Unmarshal(tagBytes, &tags); err != nil { 1816 + log.Println("failed to decode XRPC tags response", err) 1817 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1651 1818 return 1652 1819 } 1653 1820 ··· 1699 1866 return 1700 1867 } 1701 1868 1702 - us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1869 + scheme := "http" 1870 + if !rp.config.Core.Dev { 1871 + scheme = "https" 1872 + } 1873 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 1874 + xrpcc := &indigoxrpc.Client{ 1875 + Host: host, 1876 + } 1877 + 1878 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1879 + 1880 + branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1703 1881 if err != nil { 1704 - log.Printf("failed to create unsigned client for %s", f.Knot) 1705 - rp.pages.Error503(w) 1882 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1883 + log.Println("failed to call XRPC repo.branches", xrpcerr) 1884 + rp.pages.Error503(w) 1885 + return 1886 + } 1887 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1888 + return 1889 + } 1890 + 1891 + var branches types.RepoBranchesResponse 1892 + if err := json.Unmarshal(branchBytes, &branches); err != nil { 1893 + log.Println("failed to decode XRPC branches response", err) 1894 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1706 1895 return 1707 1896 } 1708 1897 1709 - branches, err := us.Branches(f.OwnerDid(), f.Name) 1898 + tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 1710 1899 if err != nil { 1900 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1901 + log.Println("failed to call XRPC repo.tags", xrpcerr) 1902 + rp.pages.Error503(w) 1903 + return 1904 + } 1711 1905 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1712 - log.Println("failed to reach knotserver", err) 1906 + return 1907 + } 1908 + 1909 + var tags types.RepoTagsResponse 1910 + if err := json.Unmarshal(tagBytes, &tags); err != nil { 1911 + log.Println("failed to decode XRPC tags response", err) 1912 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1713 1913 return 1714 1914 } 1715 1915 1716 - tags, err := us.Tags(f.OwnerDid(), f.Name) 1916 + compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head) 1717 1917 if err != nil { 1918 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1919 + log.Println("failed to call XRPC repo.compare", xrpcerr) 1920 + rp.pages.Error503(w) 1921 + return 1922 + } 1718 1923 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1719 - log.Println("failed to reach knotserver", err) 1720 1924 return 1721 1925 } 1722 1926 1723 - formatPatch, err := us.Compare(f.OwnerDid(), f.Name, base, head) 1724 - if err != nil { 1927 + var formatPatch types.RepoFormatPatchResponse 1928 + if err := json.Unmarshal(compareBytes, &formatPatch); err != nil { 1929 + log.Println("failed to decode XRPC compare response", err) 1725 1930 rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1726 - log.Println("failed to compare", err) 1727 1931 return 1728 1932 } 1933 + 1729 1934 diff := patchutil.AsNiceDiff(formatPatch.Patch, base) 1730 1935 1731 1936 repoinfo := f.RepoInfo(user)
+1 -1
appview/xrpcclient/xrpc.go
··· 115 115 116 116 var xrpcerr *indigoxrpc.Error 117 117 if ok := errors.As(err, &xrpcerr); !ok { 118 - return fmt.Errorf("Recieved invalid XRPC error response.") 118 + return fmt.Errorf("Recieved invalid XRPC error response: %v", err) 119 119 } 120 120 121 121 switch xrpcerr.StatusCode {