appview,knotserver: make ref optional in all xrpc endpoint #563

merged
opened by oppi.li targeting master from push-yqnqquktxqpx

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

+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 } 24 25 26 + ref := r.URL.Query().Get("ref") 27 + // ref can be empty (git.Open handles this) 28 + 25 29 treePath := r.URL.Query().Get("path") 26 30 if treePath == "" { 27 31 writeError(w, xrpcerr.NewXrpcError(
+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"), ··· 37 36 return 38 37 } 39 38 40 - rev1, _ := url.PathUnescape(rev1Param) 41 - rev2, _ := url.PathUnescape(rev2Param) 42 - 43 39 gr, err := git.PlainOpen(repoPath) 44 40 if err != nil { 45 41 writeError(w, xrpcerr.NewXrpcError(
+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 { ··· 27 20 return 28 21 } 29 22 23 + ref := r.URL.Query().Get("ref") 24 + 30 25 gr, err := git.Open(repoPath, ref) 31 26 if err != nil { 32 27 x.Logger.Error("opening repo", "error", err.Error())
+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") ··· 38 30 } 39 31 } 40 32 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 33 gr, err := git.Open(repoPath, ref) 51 34 if err != nil { 52 35 writeError(w, xrpcerr.NewXrpcError(
+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) ··· 119 118 return repoPath, nil 120 119 } 121 120 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 121 func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) { 145 122 w.Header().Set("Content-Type", "application/json") 146 123 w.WriteHeader(status)