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

Compare changes

Choose any two refs to compare.

+8 -4
appview/db/issues.go
··· 359 repoMap[string(repos[i].RepoAt())] = &repos[i] 360 } 361 362 - for issueAt := range issueMap { 363 - i := issueMap[issueAt] 364 - r := repoMap[string(i.RepoAt)] 365 - i.Repo = r 366 } 367 368 // collect comments
··· 359 repoMap[string(repos[i].RepoAt())] = &repos[i] 360 } 361 362 + for issueAt, i := range issueMap { 363 + if r, ok := repoMap[string(i.RepoAt)]; ok { 364 + i.Repo = r 365 + } else { 366 + // do not show up the issue if the repo is deleted 367 + // TODO: foreign key where? 368 + delete(issueMap, issueAt) 369 + } 370 } 371 372 // collect comments
+2 -2
appview/db/profile.go
··· 553 query = `select count(id) from pulls where owner_did = ? and state = ?` 554 args = append(args, did, PullOpen) 555 case VanityStatOpenIssueCount: 556 - query = `select count(id) from issues where owner_did = ? and open = 1` 557 args = append(args, did) 558 case VanityStatClosedIssueCount: 559 - query = `select count(id) from issues where owner_did = ? and open = 0` 560 args = append(args, did) 561 case VanityStatRepositoryCount: 562 query = `select count(id) from repos where did = ?`
··· 553 query = `select count(id) from pulls where owner_did = ? and state = ?` 554 args = append(args, did, PullOpen) 555 case VanityStatOpenIssueCount: 556 + query = `select count(id) from issues where did = ? and open = 1` 557 args = append(args, did) 558 case VanityStatClosedIssueCount: 559 + query = `select count(id) from issues where did = ? and open = 0` 560 args = append(args, did) 561 case VanityStatRepositoryCount: 562 query = `select count(id) from repos where did = ?`
+1 -1
appview/pages/markup/markdown.go
··· 235 repoName := fmt.Sprintf("%s/%s", rctx.RepoInfo.OwnerDid, rctx.RepoInfo.Name) 236 237 query := fmt.Sprintf("repo=%s&ref=%s&path=%s&raw=true", 238 - repoName, url.PathEscape(rctx.RepoInfo.Ref), actualPath) 239 240 parsedURL := &url.URL{ 241 Scheme: scheme,
··· 235 repoName := fmt.Sprintf("%s/%s", rctx.RepoInfo.OwnerDid, rctx.RepoInfo.Name) 236 237 query := fmt.Sprintf("repo=%s&ref=%s&path=%s&raw=true", 238 + url.PathEscape(repoName), url.PathEscape(rctx.RepoInfo.Ref), actualPath) 239 240 parsedURL := &url.URL{ 241 Scheme: scheme,
+90
appview/pages/templates/fragments/multiline-select.html
···
··· 1 + {{ define "fragments/multiline-select" }} 2 + <script> 3 + function highlight(scroll = false) { 4 + document.querySelectorAll(".hl").forEach(el => { 5 + el.classList.remove("hl"); 6 + }); 7 + 8 + const hash = window.location.hash; 9 + if (!hash || !hash.startsWith("#L")) { 10 + return; 11 + } 12 + 13 + const rangeStr = hash.substring(2); 14 + const parts = rangeStr.split("-"); 15 + let startLine, endLine; 16 + 17 + if (parts.length === 2) { 18 + startLine = parseInt(parts[0], 10); 19 + endLine = parseInt(parts[1], 10); 20 + } else { 21 + startLine = parseInt(parts[0], 10); 22 + endLine = startLine; 23 + } 24 + 25 + if (isNaN(startLine) || isNaN(endLine)) { 26 + console.log("nan"); 27 + console.log(startLine); 28 + console.log(endLine); 29 + return; 30 + } 31 + 32 + let target = null; 33 + 34 + for (let i = startLine; i<= endLine; i++) { 35 + const idEl = document.getElementById(`L${i}`); 36 + if (idEl) { 37 + const el = idEl.closest(".line"); 38 + if (el) { 39 + el.classList.add("hl"); 40 + target = el; 41 + } 42 + } 43 + } 44 + 45 + if (scroll && target) { 46 + target.scrollIntoView({ 47 + behavior: "smooth", 48 + block: "center", 49 + }); 50 + } 51 + } 52 + 53 + document.addEventListener("DOMContentLoaded", () => { 54 + console.log("DOMContentLoaded"); 55 + highlight(true); 56 + }); 57 + window.addEventListener("hashchange", () => { 58 + console.log("hashchange"); 59 + highlight(); 60 + }); 61 + window.addEventListener("popstate", () => { 62 + console.log("popstate"); 63 + highlight(); 64 + }); 65 + 66 + const lineNumbers = document.querySelectorAll('a[href^="#L"'); 67 + let startLine = null; 68 + 69 + lineNumbers.forEach(el => { 70 + el.addEventListener("click", (event) => { 71 + event.preventDefault(); 72 + const currentLine = parseInt(el.href.split("#L")[1]); 73 + 74 + if (event.shiftKey && startLine !== null) { 75 + const endLine = currentLine; 76 + const min = Math.min(startLine, endLine); 77 + const max = Math.max(startLine, endLine); 78 + const newHash = `#L${min}-${max}`; 79 + history.pushState(null, '', newHash); 80 + } else { 81 + const newHash = `#L${currentLine}`; 82 + history.pushState(null, '', newHash); 83 + startLine = currentLine; 84 + } 85 + 86 + highlight(); 87 + }); 88 + }); 89 + </script> 90 + {{ end }}
+1
appview/pages/templates/repo/blob.html
··· 78 {{ end }} 79 </div> 80 {{ end }} 81 {{ end }}
··· 78 {{ end }} 79 </div> 80 {{ end }} 81 + {{ template "fragments/multiline-select" }} 82 {{ end }}
+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
+3 -2
appview/pages/templates/strings/string.html
··· 23 hx-boost="true" 24 href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit"> 25 {{ i "pencil" "size-4" }} 26 - <span class="hidden md:inline">edit</span> 27 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 28 </a> 29 <button ··· 34 hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?" 35 > 36 {{ i "trash-2" "size-4" }} 37 - <span class="hidden md:inline">delete</span> 38 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 39 </button> 40 </div> ··· 80 <div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ .String.Contents | escapeHtml }}</div> 81 {{ end }} 82 </div> 83 </section> 84 {{ end }}
··· 23 hx-boost="true" 24 href="/strings/{{ .String.Did }}/{{ .String.Rkey }}/edit"> 25 {{ i "pencil" "size-4" }} 26 + <span class="hidden md:inline">edit</span> 27 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 28 </a> 29 <button ··· 34 hx-confirm="Are you sure you want to delete the string `{{ .String.Filename }}`?" 35 > 36 {{ i "trash-2" "size-4" }} 37 + <span class="hidden md:inline">delete</span> 38 {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 39 </button> 40 </div> ··· 80 <div id="blob-contents" class="whitespace-pre peer-target:bg-yellow-200 dark:peer-target:bg-yellow-900">{{ .String.Contents | escapeHtml }}</div> 81 {{ end }} 82 </div> 83 + {{ template "fragments/multiline-select" }} 84 </section> 85 {{ end }}
+16 -17
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 { ··· 61 RepoInfo: repoInfo, 62 }) 63 return 64 - } else { 65 - rp.pages.Error503(w) 66 - log.Println("failed to build index response", err) 67 - return 68 } 69 } 70 71 tagMap := make(map[string][]string) ··· 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 257 - if ref == "" && len(branchesResp.Branches) > 0 { 258 for _, branch := range branchesResp.Branches { 259 if branch.IsDefault { 260 ref = branch.Name 261 break 262 } 263 } 264 - if ref == "" { 265 - ref = branchesResp.Branches[0].Name 266 - } 267 } 268 269 - // check if repo is empty 270 - if len(branchesResp.Branches) == 0 { 271 return &types.RepoIndexResponse{ 272 IsEmpty: true, 273 Branches: branchesResp.Branches, ··· 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 { ··· 63 RepoInfo: repoInfo, 64 }) 65 return 66 } 67 + 68 + rp.pages.Error503(w) 69 + log.Println("failed to build index response", err) 70 + return 71 } 72 73 tagMap := make(map[string][]string) ··· 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 259 + if ref == "" { 260 for _, branch := range branchesResp.Branches { 261 if branch.IsDefault { 262 ref = branch.Name 263 break 264 } 265 } 266 } 267 268 + // if ref is still empty, this means the default branch is not set 269 + if ref == "" { 270 return &types.RepoIndexResponse{ 271 IsEmpty: true, 272 Branches: branchesResp.Branches, ··· 291 defer wg.Done() 292 tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo) 293 if err != nil { 294 + errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err)) 295 return 296 } 297 298 if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil { 299 + errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoTags: %w", err)) 300 } 301 }() 302 ··· 306 defer wg.Done() 307 resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo) 308 if err != nil { 309 + errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err)) 310 return 311 } 312 treeResp = resp ··· 318 defer wg.Done() 319 logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo) 320 if err != nil { 321 + errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err)) 322 return 323 } 324 325 if err := json.Unmarshal(logBytes, &logResp); err != nil { 326 + errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoLog: %w", err)) 327 } 328 }() 329
+98 -120
appview/repo/repo.go
··· 11 "log/slog" 12 "net/http" 13 "net/url" 14 - "path" 15 "path/filepath" 16 "slices" 17 "strconv" ··· 86 } 87 88 func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { 89 - refParam := chi.URLParam(r, "ref") 90 f, err := rp.repoResolver.Resolve(r) 91 if err != nil { 92 log.Println("failed to get repo and knot", err) ··· 103 } 104 105 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 106 - archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", refParam, repo) 107 - if err != nil { 108 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 109 - log.Println("failed to call XRPC repo.archive", xrpcerr) 110 - rp.pages.Error503(w) 111 - return 112 - } 113 - rp.pages.Error404(w) 114 return 115 } 116 117 - // Set headers for file download 118 - filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, refParam) 119 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 120 w.Header().Set("Content-Type", "application/gzip") 121 w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) ··· 140 } 141 142 ref := chi.URLParam(r, "ref") 143 144 scheme := "http" 145 if !rp.config.Core.Dev { ··· 160 161 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 162 xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo) 163 - if err != nil { 164 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 165 - log.Println("failed to call XRPC repo.log", xrpcerr) 166 - rp.pages.Error503(w) 167 - return 168 - } 169 - rp.pages.Error404(w) 170 return 171 } 172 ··· 178 } 179 180 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 181 - if err != nil { 182 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 183 - log.Println("failed to call XRPC repo.tags", xrpcerr) 184 - rp.pages.Error503(w) 185 - return 186 - } 187 } 188 189 tagMap := make(map[string][]string) ··· 197 } 198 199 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 200 - if err != nil { 201 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 202 - log.Println("failed to call XRPC repo.branches", xrpcerr) 203 - rp.pages.Error503(w) 204 - return 205 - } 206 } 207 208 if branchBytes != nil { ··· 354 return 355 } 356 ref := chi.URLParam(r, "ref") 357 358 var diffOpts types.DiffOpts 359 if d := r.URL.Query().Get("diff"); d == "split" { ··· 376 377 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 378 xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo) 379 - if err != nil { 380 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 381 - log.Println("failed to call XRPC repo.diff", xrpcerr) 382 - rp.pages.Error503(w) 383 - return 384 - } 385 - rp.pages.Error404(w) 386 return 387 } 388 ··· 434 } 435 436 ref := chi.URLParam(r, "ref") 437 - treePath := chi.URLParam(r, "*") 438 439 // if the tree path has a trailing slash, let's strip it 440 // so we don't 404 441 treePath = strings.TrimSuffix(treePath, "/") 442 443 scheme := "http" ··· 451 452 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 453 xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo) 454 - if err != nil { 455 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 456 - log.Println("failed to call XRPC repo.tree", xrpcerr) 457 - rp.pages.Error503(w) 458 - return 459 - } 460 - rp.pages.Error404(w) 461 return 462 } 463 ··· 499 500 // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated, 501 // so we can safely redirect to the "parent" (which is the same file). 502 - unescapedTreePath, _ := url.PathUnescape(treePath) 503 - if len(result.Files) == 0 && result.Parent == unescapedTreePath { 504 - http.Redirect(w, r, fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound) 505 return 506 } 507 508 user := rp.oauth.GetUser(r) 509 510 var breadcrumbs [][]string 511 - breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 512 if treePath != "" { 513 for idx, elem := range strings.Split(treePath, "/") { 514 - breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) 515 } 516 } 517 ··· 544 545 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 546 xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 547 - if err != nil { 548 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 549 - log.Println("failed to call XRPC repo.tags", xrpcerr) 550 - rp.pages.Error503(w) 551 - return 552 - } 553 - rp.pages.Error404(w) 554 return 555 } 556 ··· 617 618 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 619 xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 620 - if err != nil { 621 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 622 - log.Println("failed to call XRPC repo.branches", xrpcerr) 623 - rp.pages.Error503(w) 624 - return 625 - } 626 - rp.pages.Error404(w) 627 return 628 } 629 ··· 652 } 653 654 ref := chi.URLParam(r, "ref") 655 filePath := chi.URLParam(r, "*") 656 657 scheme := "http" 658 if !rp.config.Core.Dev { ··· 665 666 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 667 resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo) 668 - if err != nil { 669 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 670 - log.Println("failed to call XRPC repo.blob", xrpcerr) 671 - rp.pages.Error503(w) 672 - return 673 - } 674 - rp.pages.Error404(w) 675 return 676 } 677 678 // Use XRPC response directly instead of converting to internal types 679 680 var breadcrumbs [][]string 681 - breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) 682 if filePath != "" { 683 for idx, elem := range strings.Split(filePath, "/") { 684 - breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)}) 685 } 686 } 687 ··· 710 } 711 712 // fetch the raw binary content using sh.tangled.repo.blob xrpc 713 - repoName := path.Join("%s/%s", f.OwnerDid(), f.Name) 714 - blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true", 715 - scheme, f.Knot, url.QueryEscape(repoName), url.QueryEscape(ref), url.QueryEscape(filePath)) 716 717 contentSrc = blobURL 718 if !rp.config.Core.Dev { ··· 767 } 768 769 ref := chi.URLParam(r, "ref") 770 filePath := chi.URLParam(r, "*") 771 772 scheme := "http" 773 if !rp.config.Core.Dev { ··· 775 } 776 777 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 778 - blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true", 779 - scheme, f.Knot, url.QueryEscape(repo), url.QueryEscape(ref), url.QueryEscape(filePath)) 780 781 req, err := http.NewRequest("GET", blobURL, nil) 782 if err != nil { ··· 1365 1366 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1367 xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1368 - if err != nil { 1369 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1370 - log.Println("failed to call XRPC repo.branches", xrpcerr) 1371 - rp.pages.Error503(w) 1372 - return 1373 - } 1374 rp.pages.Error503(w) 1375 return 1376 } ··· 1472 1473 func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1474 ref := chi.URLParam(r, "ref") 1475 1476 user := rp.oauth.GetUser(r) 1477 f, err := rp.repoResolver.Resolve(r) ··· 1760 1761 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1762 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1763 - if err != nil { 1764 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1765 - log.Println("failed to call XRPC repo.branches", xrpcerr) 1766 - rp.pages.Error503(w) 1767 - return 1768 - } 1769 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1770 return 1771 } 1772 ··· 1801 } 1802 1803 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 1804 - if err != nil { 1805 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1806 - log.Println("failed to call XRPC repo.tags", xrpcerr) 1807 - rp.pages.Error503(w) 1808 - return 1809 - } 1810 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1811 return 1812 } 1813 ··· 1878 repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1879 1880 branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1881 - if err != nil { 1882 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1883 - log.Println("failed to call XRPC repo.branches", xrpcerr) 1884 - rp.pages.Error503(w) 1885 - return 1886 - } 1887 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1888 return 1889 } 1890 ··· 1896 } 1897 1898 tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 1899 - if err != nil { 1900 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1901 - log.Println("failed to call XRPC repo.tags", xrpcerr) 1902 - rp.pages.Error503(w) 1903 - return 1904 - } 1905 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1906 return 1907 } 1908 ··· 1914 } 1915 1916 compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head) 1917 - if err != nil { 1918 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1919 - log.Println("failed to call XRPC repo.compare", xrpcerr) 1920 - rp.pages.Error503(w) 1921 - return 1922 - } 1923 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1924 return 1925 } 1926
··· 11 "log/slog" 12 "net/http" 13 "net/url" 14 "path/filepath" 15 "slices" 16 "strconv" ··· 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 ··· 687 } 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
-1
appview/state/profile.go
··· 17 "github.com/gorilla/feeds" 18 "tangled.sh/tangled.sh/core/api/tangled" 19 "tangled.sh/tangled.sh/core/appview/db" 20 - // "tangled.sh/tangled.sh/core/appview/oauth" 21 "tangled.sh/tangled.sh/core/appview/pages" 22 ) 23
··· 17 "github.com/gorilla/feeds" 18 "tangled.sh/tangled.sh/core/api/tangled" 19 "tangled.sh/tangled.sh/core/appview/db" 20 "tangled.sh/tangled.sh/core/appview/pages" 21 ) 22
+16
default.nix
···
··· 1 + # Default setup from https://git.lix.systems/lix-project/flake-compat 2 + let 3 + lockFile = builtins.fromJSON (builtins.readFile ./flake.lock); 4 + flake-compat-node = lockFile.nodes.${lockFile.nodes.root.inputs.flake-compat}; 5 + flake-compat = builtins.fetchTarball { 6 + inherit (flake-compat-node.locked) url; 7 + sha256 = flake-compat-node.locked.narHash; 8 + }; 9 + 10 + flake = ( 11 + import flake-compat { 12 + src = ./.; 13 + } 14 + ); 15 + in 16 + flake.defaultNix
+15
flake.lock
··· 1 { 2 "nodes": { 3 "flake-utils": { 4 "inputs": { 5 "systems": "systems" ··· 136 }, 137 "root": { 138 "inputs": { 139 "gomod2nix": "gomod2nix", 140 "htmx-src": "htmx-src", 141 "htmx-ws-src": "htmx-ws-src",
··· 1 { 2 "nodes": { 3 + "flake-compat": { 4 + "flake": false, 5 + "locked": { 6 + "lastModified": 1751685974, 7 + "narHash": "sha256-NKw96t+BgHIYzHUjkTK95FqYRVKB8DHpVhefWSz/kTw=", 8 + "rev": "549f2762aebeff29a2e5ece7a7dc0f955281a1d1", 9 + "type": "tarball", 10 + "url": "https://git.lix.systems/api/v1/repos/lix-project/flake-compat/archive/549f2762aebeff29a2e5ece7a7dc0f955281a1d1.tar.gz?rev=549f2762aebeff29a2e5ece7a7dc0f955281a1d1" 11 + }, 12 + "original": { 13 + "type": "tarball", 14 + "url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz" 15 + } 16 + }, 17 "flake-utils": { 18 "inputs": { 19 "systems": "systems" ··· 150 }, 151 "root": { 152 "inputs": { 153 + "flake-compat": "flake-compat", 154 "gomod2nix": "gomod2nix", 155 "htmx-src": "htmx-src", 156 "htmx-ws-src": "htmx-ws-src",
+5
flake.nix
··· 7 url = "github:nix-community/gomod2nix"; 8 inputs.nixpkgs.follows = "nixpkgs"; 9 }; 10 indigo = { 11 url = "github:oppiliappan/indigo"; 12 flake = false; ··· 50 inter-fonts-src, 51 sqlite-lib-src, 52 ibm-plex-mono-src, 53 }: let 54 supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"]; 55 forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
··· 7 url = "github:nix-community/gomod2nix"; 8 inputs.nixpkgs.follows = "nixpkgs"; 9 }; 10 + flake-compat = { 11 + url = "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz"; 12 + flake = false; 13 + }; 14 indigo = { 15 url = "github:oppiliappan/indigo"; 16 flake = false; ··· 54 inter-fonts-src, 55 sqlite-lib-src, 56 ibm-plex-mono-src, 57 + ... 58 }: let 59 supportedSystems = ["x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin"]; 60 forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
+2 -5
input.css
··· 228 } 229 /* LineHighlight */ 230 .chroma .hl { 231 - background-color: #bcc0cc; 232 } 233 /* LineNumbersTable */ 234 .chroma .lnt { 235 white-space: pre; ··· 864 text-decoration: underline; 865 } 866 } 867 - 868 - .chroma .line:has(.ln:target) { 869 - @apply bg-amber-400/30 dark:bg-amber-500/20; 870 - }
··· 228 } 229 /* LineHighlight */ 230 .chroma .hl { 231 + @apply bg-amber-400/30 dark:bg-amber-500/20; 232 } 233 + 234 /* LineNumbersTable */ 235 .chroma .lnt { 236 white-space: pre; ··· 865 text-decoration: underline; 866 } 867 }
+1 -10
knotserver/xrpc/list_keys.go
··· 1 package xrpc 2 3 import ( 4 - "encoding/json" 5 "net/http" 6 "strconv" 7 ··· 46 response.Cursor = &nextCursor 47 } 48 49 - w.Header().Set("Content-Type", "application/json") 50 - if err := json.NewEncoder(w).Encode(response); err != nil { 51 - x.Logger.Error("failed to encode response", "error", err) 52 - writeError(w, xrpcerr.NewXrpcError( 53 - xrpcerr.WithTag("InternalServerError"), 54 - xrpcerr.WithMessage("failed to encode response"), 55 - ), http.StatusInternalServerError) 56 - return 57 - } 58 }
··· 1 package xrpc 2 3 import ( 4 "net/http" 5 "strconv" 6 ··· 45 response.Cursor = &nextCursor 46 } 47 48 + writeJson(w, response) 49 }
+1 -10
knotserver/xrpc/owner.go
··· 1 package xrpc 2 3 import ( 4 - "encoding/json" 5 "net/http" 6 7 "tangled.sh/tangled.sh/core/api/tangled" ··· 19 Owner: owner, 20 } 21 22 - w.Header().Set("Content-Type", "application/json") 23 - if err := json.NewEncoder(w).Encode(response); err != nil { 24 - x.Logger.Error("failed to encode response", "error", err) 25 - writeError(w, xrpcerr.NewXrpcError( 26 - xrpcerr.WithTag("InternalServerError"), 27 - xrpcerr.WithMessage("failed to encode response"), 28 - ), http.StatusInternalServerError) 29 - return 30 - } 31 }
··· 1 package xrpc 2 3 import ( 4 "net/http" 5 6 "tangled.sh/tangled.sh/core/api/tangled" ··· 18 Owner: owner, 19 } 20 21 + writeJson(w, response) 22 }
+8 -7
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 == "" { ··· 34 return 35 } 36 37 - gr, err := git.Open(repoPath, unescapedRef) 38 if err != nil { 39 - writeError(w, xrpcerr.NewXrpcError( 40 - xrpcerr.WithTag("RefNotFound"), 41 - xrpcerr.WithMessage("repository or ref not found"), 42 - ), http.StatusNotFound) 43 return 44 } 45 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 == "" { ··· 38 return 39 } 40 41 + gr, err := git.Open(repoPath, ref) 42 if err != nil { 43 + writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 44 return 45 } 46 47 repoParts := strings.Split(repo, "/") 48 repoName := repoParts[len(repoParts)-1] 49 50 + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 51 52 var archivePrefix string 53 if prefix != "" {
+8 -15
knotserver/xrpc/repo_blob.go
··· 3 import ( 4 "crypto/sha256" 5 "encoding/base64" 6 - "encoding/json" 7 "fmt" 8 "net/http" 9 "path/filepath" ··· 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 == "" { 27 writeError(w, xrpcerr.NewXrpcError( ··· 35 36 gr, err := git.Open(repoPath, ref) 37 if err != nil { 38 - writeError(w, xrpcerr.NewXrpcError( 39 - xrpcerr.WithTag("RefNotFound"), 40 - xrpcerr.WithMessage("repository or ref not found"), 41 - ), http.StatusNotFound) 42 return 43 } 44 ··· 69 return 70 } 71 w.Header().Set("ETag", eTag) 72 73 case strings.HasPrefix(mimeType, "text/"): 74 w.Header().Set("Cache-Control", "public, no-cache") ··· 122 response.MimeType = &mimeType 123 } 124 125 - w.Header().Set("Content-Type", "application/json") 126 - if err := json.NewEncoder(w).Encode(response); err != nil { 127 - x.Logger.Error("failed to encode response", "error", err) 128 - writeError(w, xrpcerr.NewXrpcError( 129 - xrpcerr.WithTag("InternalServerError"), 130 - xrpcerr.WithMessage("failed to encode response"), 131 - ), http.StatusInternalServerError) 132 - return 133 - } 134 } 135 136 // isTextualMimeType returns true if the MIME type represents textual content
··· 3 import ( 4 "crypto/sha256" 5 "encoding/base64" 6 "fmt" 7 "net/http" 8 "path/filepath" ··· 15 ) 16 17 func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) { 18 + repo := r.URL.Query().Get("repo") 19 + repoPath, err := x.parseRepoParam(repo) 20 if err != nil { 21 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 22 return 23 } 24 25 + ref := r.URL.Query().Get("ref") 26 + // ref can be empty (git.Open handles this) 27 + 28 treePath := r.URL.Query().Get("path") 29 if treePath == "" { 30 writeError(w, xrpcerr.NewXrpcError( ··· 38 39 gr, err := git.Open(repoPath, ref) 40 if err != nil { 41 + writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 42 return 43 } 44 ··· 69 return 70 } 71 w.Header().Set("ETag", eTag) 72 + w.Header().Set("Content-Type", mimeType) 73 74 case strings.HasPrefix(mimeType, "text/"): 75 w.Header().Set("Cache-Control", "public, no-cache") ··· 123 response.MimeType = &mimeType 124 } 125 126 + writeJson(w, response) 127 } 128 129 // isTextualMimeType returns true if the MIME type represents textual content
+5 -16
knotserver/xrpc/repo_branch.go
··· 1 package xrpc 2 3 import ( 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" ··· 31 32 gr, err := git.PlainOpen(repoPath) 33 if err != nil { 34 - writeError(w, xrpcerr.NewXrpcError( 35 - xrpcerr.WithTag("RepoNotFound"), 36 - xrpcerr.WithMessage("repository not found"), 37 - ), http.StatusNotFound) 38 return 39 } 40 ··· 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") 88 - if err := json.NewEncoder(w).Encode(response); err != nil { 89 - x.Logger.Error("failed to encode response", "error", err) 90 - writeError(w, xrpcerr.NewXrpcError( 91 - xrpcerr.WithTag("InternalServerError"), 92 - xrpcerr.WithMessage("failed to encode response"), 93 - ), http.StatusInternalServerError) 94 - return 95 - } 96 }
··· 1 package xrpc 2 3 import ( 4 "net/http" 5 "net/url" 6 + "time" 7 8 "tangled.sh/tangled.sh/core/api/tangled" 9 "tangled.sh/tangled.sh/core/knotserver/git" ··· 31 32 gr, err := git.PlainOpen(repoPath) 33 if err != nil { 34 + writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent) 35 return 36 } 37 ··· 67 Name: ref.Name().Short(), 68 Hash: ref.Hash().String(), 69 ShortHash: &[]string{ref.Hash().String()[:7]}[0], 70 + When: commit.Author.When.Format(time.RFC3339), 71 IsDefault: &isDefault, 72 } 73 ··· 78 response.Author = &tangled.RepoBranch_Signature{ 79 Name: commit.Author.Name, 80 Email: commit.Author.Email, 81 + When: commit.Author.When.Format(time.RFC3339), 82 } 83 84 + writeJson(w, response) 85 }
+11 -25
knotserver/xrpc/repo_branches.go
··· 1 package xrpc 2 3 import ( 4 - "encoding/json" 5 "net/http" 6 "strconv" 7 ··· 20 21 cursor := r.URL.Query().Get("cursor") 22 23 - limit := 50 // default 24 - if limitStr := r.URL.Query().Get("limit"); limitStr != "" { 25 - if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { 26 - limit = l 27 - } 28 - } 29 30 gr, err := git.PlainOpen(repoPath) 31 if err != nil { 32 - writeError(w, xrpcerr.NewXrpcError( 33 - xrpcerr.WithTag("RepoNotFound"), 34 - xrpcerr.WithMessage("repository not found"), 35 - ), http.StatusNotFound) 36 return 37 } 38 ··· 45 } 46 } 47 48 - end := offset + limit 49 - if end > len(branches) { 50 - end = len(branches) 51 - } 52 53 paginatedBranches := branches[offset:end] 54 ··· 57 Branches: paginatedBranches, 58 } 59 60 - // Write JSON response directly 61 - w.Header().Set("Content-Type", "application/json") 62 - if err := json.NewEncoder(w).Encode(response); err != nil { 63 - x.Logger.Error("failed to encode response", "error", err) 64 - writeError(w, xrpcerr.NewXrpcError( 65 - xrpcerr.WithTag("InternalServerError"), 66 - xrpcerr.WithMessage("failed to encode response"), 67 - ), http.StatusInternalServerError) 68 - return 69 - } 70 }
··· 1 package xrpc 2 3 import ( 4 "net/http" 5 "strconv" 6 ··· 19 20 cursor := r.URL.Query().Get("cursor") 21 22 + // limit := 50 // default 23 + // if limitStr := r.URL.Query().Get("limit"); limitStr != "" { 24 + // if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { 25 + // limit = l 26 + // } 27 + // } 28 + 29 + limit := 500 30 31 gr, err := git.PlainOpen(repoPath) 32 if err != nil { 33 + writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent) 34 return 35 } 36 ··· 43 } 44 } 45 46 + end := min(offset+limit, len(branches)) 47 48 paginatedBranches := branches[offset:end] 49 ··· 52 Branches: paginatedBranches, 53 } 54 55 + writeJson(w, response) 56 }
+7 -23
knotserver/xrpc/repo_compare.go
··· 1 package xrpc 2 3 import ( 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"), ··· 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 { 45 - writeError(w, xrpcerr.NewXrpcError( 46 - xrpcerr.WithTag("RepoNotFound"), 47 - xrpcerr.WithMessage("repository not found"), 48 - ), http.StatusNotFound) 49 return 50 } 51 ··· 79 return 80 } 81 82 - resp := types.RepoFormatPatchResponse{ 83 Rev1: commit1.Hash.String(), 84 Rev2: commit2.Hash.String(), 85 FormatPatch: formatPatch, 86 Patch: rawPatch, 87 } 88 89 - w.Header().Set("Content-Type", "application/json") 90 - if err := json.NewEncoder(w).Encode(resp); err != nil { 91 - x.Logger.Error("failed to encode response", "error", err) 92 - writeError(w, xrpcerr.NewXrpcError( 93 - xrpcerr.WithTag("InternalServerError"), 94 - xrpcerr.WithMessage("failed to encode response"), 95 - ), http.StatusInternalServerError) 96 - return 97 - } 98 }
··· 1 package xrpc 2 3 import ( 4 "fmt" 5 "net/http" 6 7 "tangled.sh/tangled.sh/core/knotserver/git" 8 "tangled.sh/tangled.sh/core/types" ··· 17 return 18 } 19 20 + rev1 := r.URL.Query().Get("rev1") 21 + if rev1 == "" { 22 writeError(w, xrpcerr.NewXrpcError( 23 xrpcerr.WithTag("InvalidRequest"), 24 xrpcerr.WithMessage("missing rev1 parameter"), ··· 26 return 27 } 28 29 + rev2 := r.URL.Query().Get("rev2") 30 + if rev2 == "" { 31 writeError(w, xrpcerr.NewXrpcError( 32 xrpcerr.WithTag("InvalidRequest"), 33 xrpcerr.WithMessage("missing rev2 parameter"), ··· 35 return 36 } 37 38 gr, err := git.PlainOpen(repoPath) 39 if err != nil { 40 + writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent) 41 return 42 } 43 ··· 71 return 72 } 73 74 + response := types.RepoFormatPatchResponse{ 75 Rev1: commit1.Hash.String(), 76 Rev2: commit2.Hash.String(), 77 FormatPatch: formatPatch, 78 Patch: rawPatch, 79 } 80 81 + writeJson(w, response) 82 }
+6 -30
knotserver/xrpc/repo_diff.go
··· 1 package xrpc 2 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 { 34 - writeError(w, xrpcerr.NewXrpcError( 35 - xrpcerr.WithTag("RefNotFound"), 36 - xrpcerr.WithMessage("repository or ref not found"), 37 - ), http.StatusNotFound) 38 return 39 } 40 41 diff, err := gr.Diff() 42 if err != nil { 43 x.Logger.Error("getting diff", "error", err.Error()) 44 - writeError(w, xrpcerr.NewXrpcError( 45 - xrpcerr.WithTag("RefNotFound"), 46 - xrpcerr.WithMessage("failed to generate diff"), 47 - ), http.StatusInternalServerError) 48 return 49 } 50 51 - resp := types.RepoCommitResponse{ 52 Ref: ref, 53 Diff: diff, 54 } 55 56 - w.Header().Set("Content-Type", "application/json") 57 - if err := json.NewEncoder(w).Encode(resp); err != nil { 58 - x.Logger.Error("failed to encode response", "error", err) 59 - writeError(w, xrpcerr.NewXrpcError( 60 - xrpcerr.WithTag("InternalServerError"), 61 - xrpcerr.WithMessage("failed to encode response"), 62 - ), http.StatusInternalServerError) 63 - return 64 - } 65 }
··· 1 package xrpc 2 3 import ( 4 "net/http" 5 6 "tangled.sh/tangled.sh/core/knotserver/git" 7 "tangled.sh/tangled.sh/core/types" ··· 16 return 17 } 18 19 + ref := r.URL.Query().Get("ref") 20 + // ref can be empty (git.Open handles this) 21 22 gr, err := git.Open(repoPath, ref) 23 if err != nil { 24 + writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 25 return 26 } 27 28 diff, err := gr.Diff() 29 if err != nil { 30 x.Logger.Error("getting diff", "error", err.Error()) 31 + writeError(w, xrpcerr.RefNotFoundError, http.StatusInternalServerError) 32 return 33 } 34 35 + response := types.RepoCommitResponse{ 36 Ref: ref, 37 Diff: diff, 38 } 39 40 + writeJson(w, response) 41 }
+4 -19
knotserver/xrpc/repo_get_default_branch.go
··· 1 package xrpc 2 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") 46 - if err := json.NewEncoder(w).Encode(response); err != nil { 47 - x.Logger.Error("failed to encode response", "error", err) 48 - writeError(w, xrpcerr.NewXrpcError( 49 - xrpcerr.WithTag("InternalServerError"), 50 - xrpcerr.WithMessage("failed to encode response"), 51 - ), http.StatusInternalServerError) 52 - return 53 - } 54 }
··· 1 package xrpc 2 3 import ( 4 "net/http" 5 + "time" 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.PlainOpen(repoPath) 21 22 branch, err := gr.FindMainBranch() 23 if err != nil { ··· 32 response := tangled.RepoGetDefaultBranch_Output{ 33 Name: branch, 34 Hash: "", 35 + When: time.UnixMicro(0).Format(time.RFC3339), 36 } 37 38 + writeJson(w, response) 39 }
+4 -21
knotserver/xrpc/repo_languages.go
··· 2 3 import ( 4 "context" 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 { ··· 27 return 28 } 29 30 gr, err := git.Open(repoPath, ref) 31 if err != nil { 32 x.Logger.Error("opening repo", "error", err.Error()) 33 - writeError(w, xrpcerr.NewXrpcError( 34 - xrpcerr.WithTag("RefNotFound"), 35 - xrpcerr.WithMessage("repository or ref not found"), 36 - ), http.StatusNotFound) 37 return 38 } 39 ··· 81 response.TotalFiles = &totalFiles 82 } 83 84 - w.Header().Set("Content-Type", "application/json") 85 - if err := json.NewEncoder(w).Encode(response); err != nil { 86 - x.Logger.Error("failed to encode response", "error", err) 87 - writeError(w, xrpcerr.NewXrpcError( 88 - xrpcerr.WithTag("InternalServerError"), 89 - xrpcerr.WithMessage("failed to encode response"), 90 - ), http.StatusInternalServerError) 91 - return 92 - } 93 }
··· 2 3 import ( 4 "context" 5 "math" 6 "net/http" 7 "time" 8 9 "tangled.sh/tangled.sh/core/api/tangled" ··· 12 ) 13 14 func (x *Xrpc) RepoLanguages(w http.ResponseWriter, r *http.Request) { 15 repo := r.URL.Query().Get("repo") 16 repoPath, err := x.parseRepoParam(repo) 17 if err != nil { ··· 19 return 20 } 21 22 + ref := r.URL.Query().Get("ref") 23 + 24 gr, err := git.Open(repoPath, ref) 25 if err != nil { 26 x.Logger.Error("opening repo", "error", err.Error()) 27 + writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 28 return 29 } 30 ··· 72 response.TotalFiles = &totalFiles 73 } 74 75 + writeJson(w, response) 76 }
+3 -33
knotserver/xrpc/repo_log.go
··· 1 package xrpc 2 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") ··· 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) 51 if err != nil { 52 - writeError(w, xrpcerr.NewXrpcError( 53 - xrpcerr.WithTag("RefNotFound"), 54 - xrpcerr.WithMessage("repository or ref not found"), 55 - ), http.StatusNotFound) 56 return 57 } 58 ··· 98 99 response.Log = true 100 101 - // Write JSON response directly 102 - w.Header().Set("Content-Type", "application/json") 103 - if err := json.NewEncoder(w).Encode(response); err != nil { 104 - x.Logger.Error("failed to encode response", "error", err) 105 - writeError(w, xrpcerr.NewXrpcError( 106 - xrpcerr.WithTag("InternalServerError"), 107 - xrpcerr.WithMessage("failed to encode response"), 108 - ), http.StatusInternalServerError) 109 - return 110 - } 111 }
··· 1 package xrpc 2 3 import ( 4 "net/http" 5 "strconv" 6 7 "tangled.sh/tangled.sh/core/knotserver/git" ··· 17 return 18 } 19 20 + ref := r.URL.Query().Get("ref") 21 22 path := r.URL.Query().Get("path") 23 cursor := r.URL.Query().Get("cursor") ··· 29 } 30 } 31 32 gr, err := git.Open(repoPath, ref) 33 if err != nil { 34 + writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 35 return 36 } 37 ··· 77 78 response.Log = true 79 80 + writeJson(w, response) 81 }
+3 -16
knotserver/xrpc/repo_tags.go
··· 1 package xrpc 2 3 import ( 4 - "encoding/json" 5 "net/http" 6 "strconv" 7 ··· 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 ··· 86 Tags: paginatedTags, 87 } 88 89 - // Write JSON response directly 90 - w.Header().Set("Content-Type", "application/json") 91 - if err := json.NewEncoder(w).Encode(response); err != nil { 92 - x.Logger.Error("failed to encode response", "error", err) 93 - writeError(w, xrpcerr.NewXrpcError( 94 - xrpcerr.WithTag("InternalServerError"), 95 - xrpcerr.WithMessage("failed to encode response"), 96 - ), http.StatusInternalServerError) 97 - return 98 - } 99 }
··· 1 package xrpc 2 3 import ( 4 "net/http" 5 "strconv" 6 ··· 29 } 30 } 31 32 + gr, err := git.PlainOpen(repoPath) 33 if err != nil { 34 x.Logger.Error("failed to open", "error", err) 35 + writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent) 36 return 37 } 38 ··· 82 Tags: paginatedTags, 83 } 84 85 + writeJson(w, response) 86 }
+6 -33
knotserver/xrpc/repo_tree.go
··· 1 package xrpc 2 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) 48 - writeError(w, xrpcerr.NewXrpcError( 49 - xrpcerr.WithTag("RefNotFound"), 50 - xrpcerr.WithMessage("repository or ref not found"), 51 - ), http.StatusNotFound) 52 return 53 } 54 ··· 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 ··· 104 Files: treeEntries, 105 } 106 107 - w.Header().Set("Content-Type", "application/json") 108 - if err := json.NewEncoder(w).Encode(response); err != nil { 109 - x.Logger.Error("failed to encode response", "error", err) 110 - writeError(w, xrpcerr.NewXrpcError( 111 - xrpcerr.WithTag("InternalServerError"), 112 - xrpcerr.WithMessage("failed to encode response"), 113 - ), http.StatusInternalServerError) 114 - return 115 - } 116 }
··· 1 package xrpc 2 3 import ( 4 "net/http" 5 "path/filepath" 6 + "time" 7 8 "tangled.sh/tangled.sh/core/api/tangled" 9 "tangled.sh/tangled.sh/core/knotserver/git" ··· 20 return 21 } 22 23 + ref := r.URL.Query().Get("ref") 24 + // ref can be empty (git.Open handles this) 25 26 path := r.URL.Query().Get("path") 27 // path can be empty (defaults to root) 28 29 gr, err := git.Open(repoPath, ref) 30 if err != nil { 31 x.Logger.Error("failed to open git repository", "error", err, "path", repoPath, "ref", ref) 32 + writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound) 33 return 34 } 35 ··· 58 entry.Last_commit = &tangled.RepoTree_LastCommit{ 59 Hash: file.LastCommit.Hash.String(), 60 Message: file.LastCommit.Message, 61 + When: file.LastCommit.When.Format(time.RFC3339), 62 } 63 } 64 ··· 85 Files: treeEntries, 86 } 87 88 + writeJson(w, response) 89 }
+1 -11
knotserver/xrpc/version.go
··· 1 package xrpc 2 3 import ( 4 - "encoding/json" 5 "fmt" 6 "net/http" 7 "runtime/debug" 8 9 "tangled.sh/tangled.sh/core/api/tangled" 10 - xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors" 11 ) 12 13 // version is set during build time. ··· 58 Version: version, 59 } 60 61 - w.Header().Set("Content-Type", "application/json") 62 - if err := json.NewEncoder(w).Encode(response); err != nil { 63 - x.Logger.Error("failed to encode response", "error", err) 64 - writeError(w, xrpcerr.NewXrpcError( 65 - xrpcerr.WithTag("InternalServerError"), 66 - xrpcerr.WithMessage("failed to encode response"), 67 - ), http.StatusInternalServerError) 68 - return 69 - } 70 }
··· 1 package xrpc 2 3 import ( 4 "fmt" 5 "net/http" 6 "runtime/debug" 7 8 "tangled.sh/tangled.sh/core/api/tangled" 9 ) 10 11 // version is set during build time. ··· 56 Version: version, 57 } 58 59 + writeJson(w, response) 60 }
+14 -35
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) 104 if err != nil { 105 - return "", xrpcerr.NewXrpcError( 106 - xrpcerr.WithTag("RepoNotFound"), 107 - xrpcerr.WithMessage("failed to access repository"), 108 - ) 109 } 110 111 repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath) 112 if err != nil { 113 - return "", xrpcerr.NewXrpcError( 114 - xrpcerr.WithTag("RepoNotFound"), 115 - xrpcerr.WithMessage("failed to access repository"), 116 - ) 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) { 145 w.Header().Set("Content-Type", "application/json") 146 w.WriteHeader(status) 147 json.NewEncoder(w).Encode(e) 148 }
··· 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) 103 if err != nil { 104 + return "", xrpcerr.RepoNotFoundError 105 } 106 107 repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath) 108 if err != nil { 109 + return "", xrpcerr.RepoNotFoundError 110 } 111 112 return repoPath, nil 113 } 114 115 func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) { 116 w.Header().Set("Content-Type", "application/json") 117 w.WriteHeader(status) 118 json.NewEncoder(w).Encode(e) 119 } 120 + 121 + func writeJson(w http.ResponseWriter, response any) { 122 + w.Header().Set("Content-Type", "application/json") 123 + if err := json.NewEncoder(w).Encode(response); err != nil { 124 + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) 125 + return 126 + } 127 + }
+10
xrpc/errors/errors.go
··· 56 WithMessage("owner not set for this service"), 57 ) 58 59 var AuthError = func(err error) XrpcError { 60 return NewXrpcError( 61 WithTag("Auth"),
··· 56 WithMessage("owner not set for this service"), 57 ) 58 59 + var RepoNotFoundError = NewXrpcError( 60 + WithTag("RepoNotFound"), 61 + WithMessage("failed to access repository"), 62 + ) 63 + 64 + var RefNotFoundError = NewXrpcError( 65 + WithTag("RefNotFound"), 66 + WithMessage("failed to access ref"), 67 + ) 68 + 69 var AuthError = func(err error) XrpcError { 70 return NewXrpcError( 71 WithTag("Auth"),