Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

oppiliappan and committed by
Tangled
8dd9e59b da5b6c9f

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