Monorepo for Tangled tangled.org

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>

oppi.li a64a4880 716f0d3d

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