forked from tangled.org/core
this repo has no description

appview,knotserver: make ref optional in all xrpc endpoint

this is backwards compatible mostly. there are bugs in the old handlers
around refs that include url escaped characters, these have been
remedied with this patch.

Signed-off-by: oppiliappan <me@oppi.li>

authored by oppi.li and committed by Tangled 8dd9e59b da5b6c9f

+3 -3
appview/pages/templates/repo/tree.html
··· 25 <div class="flex flex-col md:flex-row md:justify-between gap-2"> 26 <div id="breadcrumbs" class="overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500"> 27 {{ range .BreadCrumbs }} 28 - <a href="{{ index . 1}}" class="text-bold text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ pathUnescape (index . 0) }}</a> / 29 {{ end }} 30 </div> 31 <div id="dir-info" class="text-gray-500 dark:text-gray-400 text-xs md:text-sm flex flex-wrap items-center gap-1 md:gap-0"> 32 {{ $stats := .TreeStats }} 33 34 - <span>at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref }}">{{ $.Ref }}</a></span> 35 {{ if eq $stats.NumFolders 1 }} 36 <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 37 <span>{{ $stats.NumFolders }} folder</span> ··· 55 {{ range .Files }} 56 <div class="grid grid-cols-12 gap-4 items-center py-1"> 57 <div class="col-span-8 md:col-span-4"> 58 - {{ $link := printf "/%s/%s/%s/%s/%s" $.RepoInfo.FullName "tree" (urlquery $.Ref) $.TreePath .Name }} 59 {{ $icon := "folder" }} 60 {{ $iconStyle := "size-4 fill-current" }} 61
··· 25 <div class="flex flex-col md:flex-row md:justify-between gap-2"> 26 <div id="breadcrumbs" class="overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500"> 27 {{ range .BreadCrumbs }} 28 + <a href="{{ index . 1 }}" class="text-bold text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ pathUnescape (index . 0) }}</a> / 29 {{ end }} 30 </div> 31 <div id="dir-info" class="text-gray-500 dark:text-gray-400 text-xs md:text-sm flex flex-wrap items-center gap-1 md:gap-0"> 32 {{ $stats := .TreeStats }} 33 34 + <span>at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ pathEscape $.Ref }}">{{ $.Ref }}</a></span> 35 {{ if eq $stats.NumFolders 1 }} 36 <span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span> 37 <span>{{ $stats.NumFolders }} folder</span> ··· 55 {{ range .Files }} 56 <div class="grid grid-cols-12 gap-4 items-center py-1"> 57 <div class="col-span-8 md:col-span-4"> 58 + {{ $link := printf "/%s/%s/%s/%s/%s" $.RepoInfo.FullName "tree" (pathEscape $.Ref) $.TreePath .Name }} 59 {{ $icon := "folder" }} 60 {{ $iconStyle := "size-4 fill-current" }} 61
+9 -7
appview/repo/index.go
··· 5 "fmt" 6 "log" 7 "net/http" 8 "slices" 9 "sort" 10 "strings" ··· 31 32 func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { 33 ref := chi.URLParam(r, "ref") 34 35 f, err := rp.repoResolver.Resolve(r) 36 if err != nil { ··· 245 // first get branches to determine the ref if not specified 246 branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo) 247 if err != nil { 248 - return nil, err 249 } 250 251 var branchesResp types.RepoBranchesResponse 252 if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil { 253 - return nil, err 254 } 255 256 // if no ref specified, use default branch or first available ··· 292 defer wg.Done() 293 tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo) 294 if err != nil { 295 - errs = errors.Join(errs, err) 296 return 297 } 298 299 if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil { 300 - errs = errors.Join(errs, err) 301 } 302 }() 303 ··· 307 defer wg.Done() 308 resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo) 309 if err != nil { 310 - errs = errors.Join(errs, err) 311 return 312 } 313 treeResp = resp ··· 319 defer wg.Done() 320 logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo) 321 if err != nil { 322 - errs = errors.Join(errs, err) 323 return 324 } 325 326 if err := json.Unmarshal(logBytes, &logResp); err != nil { 327 - errs = errors.Join(errs, err) 328 } 329 }() 330
··· 5 "fmt" 6 "log" 7 "net/http" 8 + "net/url" 9 "slices" 10 "sort" 11 "strings" ··· 32 33 func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { 34 ref := chi.URLParam(r, "ref") 35 + ref, _ = url.PathUnescape(ref) 36 37 f, err := rp.repoResolver.Resolve(r) 38 if err != nil { ··· 247 // first get branches to determine the ref if not specified 248 branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo) 249 if err != nil { 250 + return nil, fmt.Errorf("failed to call repoBranches: %w", err) 251 } 252 253 var branchesResp types.RepoBranchesResponse 254 if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil { 255 + return nil, fmt.Errorf("failed to unmarshal branches response: %w", err) 256 } 257 258 // if no ref specified, use default branch or first available ··· 294 defer wg.Done() 295 tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo) 296 if err != nil { 297 + errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err)) 298 return 299 } 300 301 if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil { 302 + errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoTags: %w", err)) 303 } 304 }() 305 ··· 309 defer wg.Done() 310 resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo) 311 if err != nil { 312 + errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err)) 313 return 314 } 315 treeResp = resp ··· 321 defer wg.Done() 322 logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo) 323 if err != nil { 324 + errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err)) 325 return 326 } 327 328 if err := json.Unmarshal(logBytes, &logResp); err != nil { 329 + errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoLog: %w", err)) 330 } 331 }() 332
+97 -118
appview/repo/repo.go
··· 85 } 86 87 func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { 88 - refParam := chi.URLParam(r, "ref") 89 f, err := rp.repoResolver.Resolve(r) 90 if err != nil { 91 log.Println("failed to get repo and knot", err) ··· 102 } 103 104 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 105 - archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", refParam, repo) 106 - if err != nil { 107 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 108 - log.Println("failed to call XRPC repo.archive", xrpcerr) 109 - rp.pages.Error503(w) 110 - return 111 - } 112 - rp.pages.Error404(w) 113 return 114 } 115 116 - // Set headers for file download 117 - filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, refParam) 118 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 119 w.Header().Set("Content-Type", "application/gzip") 120 w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) ··· 139 } 140 141 ref := chi.URLParam(r, "ref") 142 143 scheme := "http" 144 if !rp.config.Core.Dev { ··· 159 160 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 161 xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo) 162 - if err != nil { 163 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 164 - log.Println("failed to call XRPC repo.log", xrpcerr) 165 - rp.pages.Error503(w) 166 - return 167 - } 168 - rp.pages.Error404(w) 169 return 170 } 171 ··· 177 } 178 179 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 180 - if err != nil { 181 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 182 - log.Println("failed to call XRPC repo.tags", xrpcerr) 183 - rp.pages.Error503(w) 184 - return 185 - } 186 } 187 188 tagMap := make(map[string][]string) ··· 196 } 197 198 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 199 - if err != nil { 200 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 201 - log.Println("failed to call XRPC repo.branches", xrpcerr) 202 - rp.pages.Error503(w) 203 - return 204 - } 205 } 206 207 if branchBytes != nil { ··· 353 return 354 } 355 ref := chi.URLParam(r, "ref") 356 357 var diffOpts types.DiffOpts 358 if d := r.URL.Query().Get("diff"); d == "split" { ··· 375 376 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 377 xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo) 378 - if err != nil { 379 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 380 - log.Println("failed to call XRPC repo.diff", xrpcerr) 381 - rp.pages.Error503(w) 382 - return 383 - } 384 - rp.pages.Error404(w) 385 return 386 } 387 ··· 433 } 434 435 ref := chi.URLParam(r, "ref") 436 - treePath := chi.URLParam(r, "*") 437 438 // if the tree path has a trailing slash, let's strip it 439 // so we don't 404 440 treePath = strings.TrimSuffix(treePath, "/") 441 442 scheme := "http" ··· 450 451 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 452 xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo) 453 - if err != nil { 454 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 455 - log.Println("failed to call XRPC repo.tree", xrpcerr) 456 - rp.pages.Error503(w) 457 - return 458 - } 459 - rp.pages.Error404(w) 460 return 461 } 462 ··· 498 499 // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated, 500 // so we can safely redirect to the "parent" (which is the same file). 501 - unescapedTreePath, _ := url.PathUnescape(treePath) 502 - if len(result.Files) == 0 && result.Parent == unescapedTreePath { 503 - http.Redirect(w, r, fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound) 504 return 505 } 506 507 user := rp.oauth.GetUser(r) 508 509 var breadcrumbs [][]string 510 - breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 511 if treePath != "" { 512 for idx, elem := range strings.Split(treePath, "/") { 513 - breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) 514 } 515 } 516 ··· 543 544 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 545 xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 546 - if err != nil { 547 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 548 - log.Println("failed to call XRPC repo.tags", xrpcerr) 549 - rp.pages.Error503(w) 550 - return 551 - } 552 - rp.pages.Error404(w) 553 return 554 } 555 ··· 616 617 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 618 xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 619 - if err != nil { 620 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 621 - log.Println("failed to call XRPC repo.branches", xrpcerr) 622 - rp.pages.Error503(w) 623 - return 624 - } 625 - rp.pages.Error404(w) 626 return 627 } 628 ··· 651 } 652 653 ref := chi.URLParam(r, "ref") 654 filePath := chi.URLParam(r, "*") 655 656 scheme := "http" 657 if !rp.config.Core.Dev { ··· 664 665 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 666 resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo) 667 - if err != nil { 668 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 669 - log.Println("failed to call XRPC repo.blob", xrpcerr) 670 - rp.pages.Error503(w) 671 - return 672 - } 673 - rp.pages.Error404(w) 674 return 675 } 676 677 // Use XRPC response directly instead of converting to internal types 678 679 var breadcrumbs [][]string 680 - breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 681 if filePath != "" { 682 for idx, elem := range strings.Split(filePath, "/") { 683 - breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) 684 } 685 } 686 ··· 710 711 // fetch the raw binary content using sh.tangled.repo.blob xrpc 712 repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 713 - blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true", 714 - scheme, f.Knot, url.QueryEscape(repoName), url.QueryEscape(ref), url.QueryEscape(filePath)) 715 716 contentSrc = blobURL 717 if !rp.config.Core.Dev { ··· 766 } 767 768 ref := chi.URLParam(r, "ref") 769 filePath := chi.URLParam(r, "*") 770 771 scheme := "http" 772 if !rp.config.Core.Dev { ··· 774 } 775 776 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 777 - blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true", 778 - scheme, f.Knot, url.QueryEscape(repo), url.QueryEscape(ref), url.QueryEscape(filePath)) 779 780 req, err := http.NewRequest("GET", blobURL, nil) 781 if err != nil { ··· 1364 1365 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1366 xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1367 - if err != nil { 1368 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1369 - log.Println("failed to call XRPC repo.branches", xrpcerr) 1370 - rp.pages.Error503(w) 1371 - return 1372 - } 1373 rp.pages.Error503(w) 1374 return 1375 } ··· 1471 1472 func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1473 ref := chi.URLParam(r, "ref") 1474 1475 user := rp.oauth.GetUser(r) 1476 f, err := rp.repoResolver.Resolve(r) ··· 1759 1760 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1761 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1762 - if err != nil { 1763 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1764 - log.Println("failed to call XRPC repo.branches", xrpcerr) 1765 - rp.pages.Error503(w) 1766 - return 1767 - } 1768 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1769 return 1770 } 1771 ··· 1800 } 1801 1802 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 1803 - if err != nil { 1804 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1805 - log.Println("failed to call XRPC repo.tags", xrpcerr) 1806 - rp.pages.Error503(w) 1807 - return 1808 - } 1809 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1810 return 1811 } 1812 ··· 1877 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1878 1879 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1880 - if err != nil { 1881 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1882 - log.Println("failed to call XRPC repo.branches", xrpcerr) 1883 - rp.pages.Error503(w) 1884 - return 1885 - } 1886 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1887 return 1888 } 1889 ··· 1895 } 1896 1897 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 1898 - if err != nil { 1899 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1900 - log.Println("failed to call XRPC repo.tags", xrpcerr) 1901 - rp.pages.Error503(w) 1902 - return 1903 - } 1904 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1905 return 1906 } 1907 ··· 1913 } 1914 1915 compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head) 1916 - if err != nil { 1917 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1918 - log.Println("failed to call XRPC repo.compare", xrpcerr) 1919 - rp.pages.Error503(w) 1920 - return 1921 - } 1922 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1923 return 1924 } 1925
··· 85 } 86 87 func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { 88 + ref := chi.URLParam(r, "ref") 89 + ref, _ = url.PathUnescape(ref) 90 + 91 f, err := rp.repoResolver.Resolve(r) 92 if err != nil { 93 log.Println("failed to get repo and knot", err) ··· 104 } 105 106 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 107 + archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo) 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 114 + // Set headers for file download, just pass along whatever the knot specifies 115 + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 116 + filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename) 117 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 118 w.Header().Set("Content-Type", "application/gzip") 119 w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) ··· 138 } 139 140 ref := chi.URLParam(r, "ref") 141 + ref, _ = url.PathUnescape(ref) 142 143 scheme := "http" 144 if !rp.config.Core.Dev { ··· 159 160 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 161 xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo) 162 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 163 + log.Println("failed to call XRPC repo.log", xrpcerr) 164 + rp.pages.Error503(w) 165 return 166 } 167 ··· 173 } 174 175 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 176 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 177 + log.Println("failed to call XRPC repo.tags", xrpcerr) 178 + rp.pages.Error503(w) 179 + return 180 } 181 182 tagMap := make(map[string][]string) ··· 190 } 191 192 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 193 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 194 + log.Println("failed to call XRPC repo.branches", xrpcerr) 195 + rp.pages.Error503(w) 196 + return 197 } 198 199 if branchBytes != nil { ··· 345 return 346 } 347 ref := chi.URLParam(r, "ref") 348 + ref, _ = url.PathUnescape(ref) 349 350 var diffOpts types.DiffOpts 351 if d := r.URL.Query().Get("diff"); d == "split" { ··· 368 369 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 370 xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo) 371 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 372 + log.Println("failed to call XRPC repo.diff", xrpcerr) 373 + rp.pages.Error503(w) 374 return 375 } 376 ··· 422 } 423 424 ref := chi.URLParam(r, "ref") 425 + ref, _ = url.PathUnescape(ref) 426 427 // if the tree path has a trailing slash, let's strip it 428 // so we don't 404 429 + treePath := chi.URLParam(r, "*") 430 + treePath, _ = url.PathUnescape(treePath) 431 treePath = strings.TrimSuffix(treePath, "/") 432 433 scheme := "http" ··· 441 442 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 443 xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo) 444 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 445 + log.Println("failed to call XRPC repo.tree", xrpcerr) 446 + rp.pages.Error503(w) 447 return 448 } 449 ··· 485 486 // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated, 487 // so we can safely redirect to the "parent" (which is the same file). 488 + if len(result.Files) == 0 && result.Parent == treePath { 489 + redirectTo := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), url.PathEscape(ref), result.Parent) 490 + http.Redirect(w, r, redirectTo, http.StatusFound) 491 return 492 } 493 494 user := rp.oauth.GetUser(r) 495 496 var breadcrumbs [][]string 497 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))}) 498 if treePath != "" { 499 for idx, elem := range strings.Split(treePath, "/") { 500 + breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))}) 501 } 502 } 503 ··· 530 531 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 532 xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 533 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 534 + log.Println("failed to call XRPC repo.tags", xrpcerr) 535 + rp.pages.Error503(w) 536 return 537 } 538 ··· 599 600 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 601 xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 602 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 603 + log.Println("failed to call XRPC repo.branches", xrpcerr) 604 + rp.pages.Error503(w) 605 return 606 } 607 ··· 630 } 631 632 ref := chi.URLParam(r, "ref") 633 + ref, _ = url.PathUnescape(ref) 634 + 635 filePath := chi.URLParam(r, "*") 636 + filePath, _ = url.PathUnescape(filePath) 637 638 scheme := "http" 639 if !rp.config.Core.Dev { ··· 646 647 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 648 resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo) 649 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 650 + log.Println("failed to call XRPC repo.blob", xrpcerr) 651 + rp.pages.Error503(w) 652 return 653 } 654 655 // Use XRPC response directly instead of converting to internal types 656 657 var breadcrumbs [][]string 658 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))}) 659 if filePath != "" { 660 for idx, elem := range strings.Split(filePath, "/") { 661 + breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))}) 662 } 663 } 664 ··· 688 689 // fetch the raw binary content using sh.tangled.repo.blob xrpc 690 repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 691 + 692 + baseURL := &url.URL{ 693 + Scheme: scheme, 694 + Host: f.Knot, 695 + Path: "/xrpc/sh.tangled.repo.blob", 696 + } 697 + query := baseURL.Query() 698 + query.Set("repo", repoName) 699 + query.Set("ref", ref) 700 + query.Set("path", filePath) 701 + query.Set("raw", "true") 702 + baseURL.RawQuery = query.Encode() 703 + blobURL := baseURL.String() 704 705 contentSrc = blobURL 706 if !rp.config.Core.Dev { ··· 755 } 756 757 ref := chi.URLParam(r, "ref") 758 + ref, _ = url.PathUnescape(ref) 759 + 760 filePath := chi.URLParam(r, "*") 761 + filePath, _ = url.PathUnescape(filePath) 762 763 scheme := "http" 764 if !rp.config.Core.Dev { ··· 766 } 767 768 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 769 + baseURL := &url.URL{ 770 + Scheme: scheme, 771 + Host: f.Knot, 772 + Path: "/xrpc/sh.tangled.repo.blob", 773 + } 774 + query := baseURL.Query() 775 + query.Set("repo", repo) 776 + query.Set("ref", ref) 777 + query.Set("path", filePath) 778 + query.Set("raw", "true") 779 + baseURL.RawQuery = query.Encode() 780 + blobURL := baseURL.String() 781 782 req, err := http.NewRequest("GET", blobURL, nil) 783 if err != nil { ··· 1366 1367 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1368 xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 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 } ··· 1469 1470 func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1471 ref := chi.URLParam(r, "ref") 1472 + ref, _ = url.PathUnescape(ref) 1473 1474 user := rp.oauth.GetUser(r) 1475 f, err := rp.repoResolver.Resolve(r) ··· 1758 1759 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1760 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1761 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1762 + log.Println("failed to call XRPC repo.branches", xrpcerr) 1763 + rp.pages.Error503(w) 1764 return 1765 } 1766 ··· 1795 } 1796 1797 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 1798 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1799 + log.Println("failed to call XRPC repo.tags", xrpcerr) 1800 + rp.pages.Error503(w) 1801 return 1802 } 1803 ··· 1868 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1869 1870 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1871 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1872 + log.Println("failed to call XRPC repo.branches", xrpcerr) 1873 + rp.pages.Error503(w) 1874 return 1875 } 1876 ··· 1882 } 1883 1884 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 1885 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1886 + log.Println("failed to call XRPC repo.tags", xrpcerr) 1887 + rp.pages.Error503(w) 1888 return 1889 } 1890 ··· 1896 } 1897 1898 compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head) 1899 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1900 + log.Println("failed to call XRPC repo.compare", xrpcerr) 1901 + rp.pages.Error503(w) 1902 return 1903 } 1904
+7 -3
knotserver/xrpc/repo_archive.go
··· 13 ) 14 15 func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) { 16 - repo, repoPath, unescapedRef, err := x.parseStandardParams(r) 17 if err != nil { 18 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 19 return 20 } 21 22 format := r.URL.Query().Get("format") 23 if format == "" { 24 format = "tar.gz" // default ··· 34 return 35 } 36 37 - gr, err := git.Open(repoPath, unescapedRef) 38 if err != nil { 39 writeError(w, xrpcerr.NewXrpcError( 40 xrpcerr.WithTag("RefNotFound"), ··· 46 repoParts := strings.Split(repo, "/") 47 repoName := repoParts[len(repoParts)-1] 48 49 - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-") 50 51 var archivePrefix string 52 if prefix != "" {
··· 13 ) 14 15 func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) { 16 + repo := r.URL.Query().Get("repo") 17 + repoPath, err := x.parseRepoParam(repo) 18 if err != nil { 19 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 20 return 21 } 22 23 + ref := r.URL.Query().Get("ref") 24 + // ref can be empty (git.Open handles this) 25 + 26 format := r.URL.Query().Get("format") 27 if format == "" { 28 format = "tar.gz" // default ··· 38 return 39 } 40 41 + gr, err := git.Open(repoPath, ref) 42 if err != nil { 43 writeError(w, xrpcerr.NewXrpcError( 44 xrpcerr.WithTag("RefNotFound"), ··· 50 repoParts := strings.Split(repo, "/") 51 repoName := repoParts[len(repoParts)-1] 52 53 + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 54 55 var archivePrefix string 56 if prefix != "" {
+5 -1
knotserver/xrpc/repo_blob.go
··· 16 ) 17 18 func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) { 19 - _, repoPath, ref, err := x.parseStandardParams(r) 20 if err != nil { 21 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 22 return 23 } 24 25 treePath := r.URL.Query().Get("path") 26 if treePath == "" {
··· 16 ) 17 18 func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) { 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 treePath := r.URL.Query().Get("path") 30 if treePath == "" {
+3 -2
knotserver/xrpc/repo_branch.go
··· 4 "encoding/json" 5 "net/http" 6 "net/url" 7 8 "tangled.sh/tangled.sh/core/api/tangled" 9 "tangled.sh/tangled.sh/core/knotserver/git" ··· 70 Name: ref.Name().Short(), 71 Hash: ref.Hash().String(), 72 ShortHash: &[]string{ref.Hash().String()[:7]}[0], 73 - When: commit.Author.When.Format("2006-01-02T15:04:05.000Z"), 74 IsDefault: &isDefault, 75 } 76 ··· 81 response.Author = &tangled.RepoBranch_Signature{ 82 Name: commit.Author.Name, 83 Email: commit.Author.Email, 84 - When: commit.Author.When.Format("2006-01-02T15:04:05.000Z"), 85 } 86 87 w.Header().Set("Content-Type", "application/json")
··· 4 "encoding/json" 5 "net/http" 6 "net/url" 7 + "time" 8 9 "tangled.sh/tangled.sh/core/api/tangled" 10 "tangled.sh/tangled.sh/core/knotserver/git" ··· 71 Name: ref.Name().Short(), 72 Hash: ref.Hash().String(), 73 ShortHash: &[]string{ref.Hash().String()[:7]}[0], 74 + When: commit.Author.When.Format(time.RFC3339), 75 IsDefault: &isDefault, 76 } 77 ··· 82 response.Author = &tangled.RepoBranch_Signature{ 83 Name: commit.Author.Name, 84 Email: commit.Author.Email, 85 + When: commit.Author.When.Format(time.RFC3339), 86 } 87 88 w.Header().Set("Content-Type", "application/json")
+1 -4
knotserver/xrpc/repo_branches.go
··· 47 } 48 } 49 50 - end := offset + limit 51 - if end > len(branches) { 52 - end = len(branches) 53 - } 54 55 paginatedBranches := branches[offset:end] 56
··· 47 } 48 } 49 50 + end := min(offset+limit, len(branches)) 51 52 paginatedBranches := branches[offset:end] 53
+4 -8
knotserver/xrpc/repo_compare.go
··· 4 "encoding/json" 5 "fmt" 6 "net/http" 7 - "net/url" 8 9 "tangled.sh/tangled.sh/core/knotserver/git" 10 "tangled.sh/tangled.sh/core/types" ··· 19 return 20 } 21 22 - rev1Param := r.URL.Query().Get("rev1") 23 - if rev1Param == "" { 24 writeError(w, xrpcerr.NewXrpcError( 25 xrpcerr.WithTag("InvalidRequest"), 26 xrpcerr.WithMessage("missing rev1 parameter"), ··· 28 return 29 } 30 31 - rev2Param := r.URL.Query().Get("rev2") 32 - if rev2Param == "" { 33 writeError(w, xrpcerr.NewXrpcError( 34 xrpcerr.WithTag("InvalidRequest"), 35 xrpcerr.WithMessage("missing rev2 parameter"), 36 ), http.StatusBadRequest) 37 return 38 } 39 - 40 - rev1, _ := url.PathUnescape(rev1Param) 41 - rev2, _ := url.PathUnescape(rev2Param) 42 43 gr, err := git.PlainOpen(repoPath) 44 if err != nil {
··· 4 "encoding/json" 5 "fmt" 6 "net/http" 7 8 "tangled.sh/tangled.sh/core/knotserver/git" 9 "tangled.sh/tangled.sh/core/types" ··· 18 return 19 } 20 21 + rev1 := r.URL.Query().Get("rev1") 22 + if rev1 == "" { 23 writeError(w, xrpcerr.NewXrpcError( 24 xrpcerr.WithTag("InvalidRequest"), 25 xrpcerr.WithMessage("missing rev1 parameter"), ··· 27 return 28 } 29 30 + rev2 := r.URL.Query().Get("rev2") 31 + if rev2 == "" { 32 writeError(w, xrpcerr.NewXrpcError( 33 xrpcerr.WithTag("InvalidRequest"), 34 xrpcerr.WithMessage("missing rev2 parameter"), 35 ), http.StatusBadRequest) 36 return 37 } 38 39 gr, err := git.PlainOpen(repoPath) 40 if err != nil {
+2 -11
knotserver/xrpc/repo_diff.go
··· 3 import ( 4 "encoding/json" 5 "net/http" 6 - "net/url" 7 8 "tangled.sh/tangled.sh/core/knotserver/git" 9 "tangled.sh/tangled.sh/core/types" ··· 18 return 19 } 20 21 - refParam := r.URL.Query().Get("ref") 22 - if refParam == "" { 23 - writeError(w, xrpcerr.NewXrpcError( 24 - xrpcerr.WithTag("InvalidRequest"), 25 - xrpcerr.WithMessage("missing ref parameter"), 26 - ), http.StatusBadRequest) 27 - return 28 - } 29 - 30 - ref, _ := url.QueryUnescape(refParam) 31 32 gr, err := git.Open(repoPath, ref) 33 if err != nil {
··· 3 import ( 4 "encoding/json" 5 "net/http" 6 7 "tangled.sh/tangled.sh/core/knotserver/git" 8 "tangled.sh/tangled.sh/core/types" ··· 17 return 18 } 19 20 + ref := r.URL.Query().Get("ref") 21 + // ref can be empty (git.Open handles this) 22 23 gr, err := git.Open(repoPath, ref) 24 if err != nil {
+3 -9
knotserver/xrpc/repo_get_default_branch.go
··· 3 import ( 4 "encoding/json" 5 "net/http" 6 7 "tangled.sh/tangled.sh/core/api/tangled" 8 "tangled.sh/tangled.sh/core/knotserver/git" ··· 17 return 18 } 19 20 - gr, err := git.Open(repoPath, "") 21 - if err != nil { 22 - writeError(w, xrpcerr.NewXrpcError( 23 - xrpcerr.WithTag("RepoNotFound"), 24 - xrpcerr.WithMessage("repository not found"), 25 - ), http.StatusNotFound) 26 - return 27 - } 28 29 branch, err := gr.FindMainBranch() 30 if err != nil { ··· 39 response := tangled.RepoGetDefaultBranch_Output{ 40 Name: branch, 41 Hash: "", 42 - When: "1970-01-01T00:00:00.000Z", 43 } 44 45 w.Header().Set("Content-Type", "application/json")
··· 3 import ( 4 "encoding/json" 5 "net/http" 6 + "time" 7 8 "tangled.sh/tangled.sh/core/api/tangled" 9 "tangled.sh/tangled.sh/core/knotserver/git" ··· 18 return 19 } 20 21 + gr, err := git.PlainOpen(repoPath) 22 23 branch, err := gr.FindMainBranch() 24 if err != nil { ··· 33 response := tangled.RepoGetDefaultBranch_Output{ 34 Name: branch, 35 Hash: "", 36 + When: time.UnixMicro(0).Format(time.RFC3339), 37 } 38 39 w.Header().Set("Content-Type", "application/json")
+2 -7
knotserver/xrpc/repo_languages.go
··· 5 "encoding/json" 6 "math" 7 "net/http" 8 - "net/url" 9 "time" 10 11 "tangled.sh/tangled.sh/core/api/tangled" ··· 14 ) 15 16 func (x *Xrpc) RepoLanguages(w http.ResponseWriter, r *http.Request) { 17 - refParam := r.URL.Query().Get("ref") 18 - if refParam == "" { 19 - refParam = "HEAD" // default 20 - } 21 - ref, _ := url.PathUnescape(refParam) 22 - 23 repo := r.URL.Query().Get("repo") 24 repoPath, err := x.parseRepoParam(repo) 25 if err != nil { 26 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 27 return 28 } 29 30 gr, err := git.Open(repoPath, ref) 31 if err != nil {
··· 5 "encoding/json" 6 "math" 7 "net/http" 8 "time" 9 10 "tangled.sh/tangled.sh/core/api/tangled" ··· 13 ) 14 15 func (x *Xrpc) RepoLanguages(w http.ResponseWriter, r *http.Request) { 16 repo := r.URL.Query().Get("repo") 17 repoPath, err := x.parseRepoParam(repo) 18 if err != nil { 19 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 20 return 21 } 22 + 23 + ref := r.URL.Query().Get("ref") 24 25 gr, err := git.Open(repoPath, ref) 26 if err != nil {
+1 -18
knotserver/xrpc/repo_log.go
··· 3 import ( 4 "encoding/json" 5 "net/http" 6 - "net/url" 7 "strconv" 8 9 "tangled.sh/tangled.sh/core/knotserver/git" ··· 19 return 20 } 21 22 - refParam := r.URL.Query().Get("ref") 23 - if refParam == "" { 24 - writeError(w, xrpcerr.NewXrpcError( 25 - xrpcerr.WithTag("InvalidRequest"), 26 - xrpcerr.WithMessage("missing ref parameter"), 27 - ), http.StatusBadRequest) 28 - return 29 - } 30 31 path := r.URL.Query().Get("path") 32 cursor := r.URL.Query().Get("cursor") ··· 36 if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { 37 limit = l 38 } 39 - } 40 - 41 - ref, err := url.QueryUnescape(refParam) 42 - if err != nil { 43 - writeError(w, xrpcerr.NewXrpcError( 44 - xrpcerr.WithTag("InvalidRequest"), 45 - xrpcerr.WithMessage("invalid ref parameter"), 46 - ), http.StatusBadRequest) 47 - return 48 } 49 50 gr, err := git.Open(repoPath, ref)
··· 3 import ( 4 "encoding/json" 5 "net/http" 6 "strconv" 7 8 "tangled.sh/tangled.sh/core/knotserver/git" ··· 18 return 19 } 20 21 + ref := r.URL.Query().Get("ref") 22 23 path := r.URL.Query().Get("path") 24 cursor := r.URL.Query().Get("cursor") ··· 28 if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { 29 limit = l 30 } 31 } 32 33 gr, err := git.Open(repoPath, ref)
+2 -2
knotserver/xrpc/repo_tags.go
··· 30 } 31 } 32 33 - gr, err := git.Open(repoPath, "") 34 if err != nil { 35 x.Logger.Error("failed to open", "error", err) 36 writeError(w, xrpcerr.NewXrpcError( 37 xrpcerr.WithTag("RepoNotFound"), 38 xrpcerr.WithMessage("repository not found"), 39 - ), http.StatusNotFound) 40 return 41 } 42
··· 30 } 31 } 32 33 + gr, err := git.PlainOpen(repoPath) 34 if err != nil { 35 x.Logger.Error("failed to open", "error", err) 36 writeError(w, xrpcerr.NewXrpcError( 37 xrpcerr.WithTag("RepoNotFound"), 38 xrpcerr.WithMessage("repository not found"), 39 + ), http.StatusNoContent) 40 return 41 } 42
+4 -19
knotserver/xrpc/repo_tree.go
··· 3 import ( 4 "encoding/json" 5 "net/http" 6 - "net/url" 7 "path/filepath" 8 9 "tangled.sh/tangled.sh/core/api/tangled" 10 "tangled.sh/tangled.sh/core/knotserver/git" ··· 21 return 22 } 23 24 - refParam := r.URL.Query().Get("ref") 25 - if refParam == "" { 26 - writeError(w, xrpcerr.NewXrpcError( 27 - xrpcerr.WithTag("InvalidRequest"), 28 - xrpcerr.WithMessage("missing ref parameter"), 29 - ), http.StatusBadRequest) 30 - return 31 - } 32 33 path := r.URL.Query().Get("path") 34 // path can be empty (defaults to root) 35 36 - ref, err := url.QueryUnescape(refParam) 37 - if err != nil { 38 - writeError(w, xrpcerr.NewXrpcError( 39 - xrpcerr.WithTag("InvalidRequest"), 40 - xrpcerr.WithMessage("invalid ref parameter"), 41 - ), http.StatusBadRequest) 42 - return 43 - } 44 - 45 gr, err := git.Open(repoPath, ref) 46 if err != nil { 47 x.Logger.Error("failed to open git repository", "error", err, "path", repoPath, "ref", ref) ··· 77 entry.Last_commit = &tangled.RepoTree_LastCommit{ 78 Hash: file.LastCommit.Hash.String(), 79 Message: file.LastCommit.Message, 80 - When: file.LastCommit.When.Format("2006-01-02T15:04:05.000Z"), 81 } 82 } 83
··· 3 import ( 4 "encoding/json" 5 "net/http" 6 "path/filepath" 7 + "time" 8 9 "tangled.sh/tangled.sh/core/api/tangled" 10 "tangled.sh/tangled.sh/core/knotserver/git" ··· 21 return 22 } 23 24 + ref := r.URL.Query().Get("ref") 25 + // ref can be empty (git.Open handles this) 26 27 path := r.URL.Query().Get("path") 28 // path can be empty (defaults to root) 29 30 gr, err := git.Open(repoPath, ref) 31 if err != nil { 32 x.Logger.Error("failed to open git repository", "error", err, "path", repoPath, "ref", ref) ··· 62 entry.Last_commit = &tangled.RepoTree_LastCommit{ 63 Hash: file.LastCommit.Hash.String(), 64 Message: file.LastCommit.Message, 65 + When: file.LastCommit.When.Format(time.RFC3339), 66 } 67 } 68
+4 -27
knotserver/xrpc/xrpc.go
··· 4 "encoding/json" 5 "log/slog" 6 "net/http" 7 - "net/url" 8 "strings" 9 10 securejoin "github.com/cyphar/filepath-securejoin" ··· 88 } 89 90 // Parse repo string (did/repoName format) 91 - parts := strings.Split(repo, "/") 92 - if len(parts) < 2 { 93 return "", xrpcerr.NewXrpcError( 94 xrpcerr.WithTag("InvalidRequest"), 95 xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"), 96 ) 97 } 98 99 - did := strings.Join(parts[:len(parts)-1], "/") 100 - repoName := parts[len(parts)-1] 101 102 // Construct repository path using the same logic as didPath 103 didRepoPath, err := securejoin.SecureJoin(did, repoName) ··· 117 } 118 119 return repoPath, nil 120 - } 121 - 122 - // parseStandardParams parses common query parameters used by most handlers 123 - func (x *Xrpc) parseStandardParams(r *http.Request) (repo, repoPath, ref string, err error) { 124 - // Parse repo parameter 125 - repo = r.URL.Query().Get("repo") 126 - repoPath, err = x.parseRepoParam(repo) 127 - if err != nil { 128 - return "", "", "", err 129 - } 130 - 131 - // Parse and unescape ref parameter 132 - refParam := r.URL.Query().Get("ref") 133 - if refParam == "" { 134 - return "", "", "", xrpcerr.NewXrpcError( 135 - xrpcerr.WithTag("InvalidRequest"), 136 - xrpcerr.WithMessage("missing ref parameter"), 137 - ) 138 - } 139 - 140 - ref, _ = url.QueryUnescape(refParam) 141 - return repo, repoPath, ref, nil 142 } 143 144 func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
··· 4 "encoding/json" 5 "log/slog" 6 "net/http" 7 "strings" 8 9 securejoin "github.com/cyphar/filepath-securejoin" ··· 87 } 88 89 // Parse repo string (did/repoName format) 90 + parts := strings.SplitN(repo, "/", 2) 91 + if len(parts) != 2 { 92 return "", xrpcerr.NewXrpcError( 93 xrpcerr.WithTag("InvalidRequest"), 94 xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"), 95 ) 96 } 97 98 + did := parts[0] 99 + repoName := parts[1] 100 101 // Construct repository path using the same logic as didPath 102 didRepoPath, err := securejoin.SecureJoin(did, repoName) ··· 116 } 117 118 return repoPath, nil 119 } 120 121 func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {