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
···
<div class="flex flex-col md:flex-row md:justify-between gap-2">
<div id="breadcrumbs" class="overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500">
{{ range .BreadCrumbs }}
-
<a href="{{ index . 1}}" class="text-bold text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ pathUnescape (index . 0) }}</a> /
{{ end }}
</div>
<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">
{{ $stats := .TreeStats }}
-
<span>at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ $.Ref }}">{{ $.Ref }}</a></span>
{{ if eq $stats.NumFolders 1 }}
<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
<span>{{ $stats.NumFolders }} folder</span>
···
{{ range .Files }}
<div class="grid grid-cols-12 gap-4 items-center py-1">
<div class="col-span-8 md:col-span-4">
-
{{ $link := printf "/%s/%s/%s/%s/%s" $.RepoInfo.FullName "tree" (urlquery $.Ref) $.TreePath .Name }}
{{ $icon := "folder" }}
{{ $iconStyle := "size-4 fill-current" }}
···
<div class="flex flex-col md:flex-row md:justify-between gap-2">
<div id="breadcrumbs" class="overflow-x-auto whitespace-nowrap text-gray-400 dark:text-gray-500">
{{ range .BreadCrumbs }}
+
<a href="{{ index . 1 }}" class="text-bold text-gray-500 dark:text-gray-400 {{ $linkstyle }}">{{ pathUnescape (index . 0) }}</a> /
{{ end }}
</div>
<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">
{{ $stats := .TreeStats }}
+
<span>at <a href="/{{ $.RepoInfo.FullName }}/tree/{{ pathEscape $.Ref }}">{{ $.Ref }}</a></span>
{{ if eq $stats.NumFolders 1 }}
<span class="select-none px-1 md:px-2 [&:before]:content-['·']"></span>
<span>{{ $stats.NumFolders }} folder</span>
···
{{ range .Files }}
<div class="grid grid-cols-12 gap-4 items-center py-1">
<div class="col-span-8 md:col-span-4">
+
{{ $link := printf "/%s/%s/%s/%s/%s" $.RepoInfo.FullName "tree" (pathEscape $.Ref) $.TreePath .Name }}
{{ $icon := "folder" }}
{{ $iconStyle := "size-4 fill-current" }}
+9 -7
appview/repo/index.go
···
"fmt"
"log"
"net/http"
"slices"
"sort"
"strings"
···
func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
ref := chi.URLParam(r, "ref")
f, err := rp.repoResolver.Resolve(r)
if err != nil {
···
// first get branches to determine the ref if not specified
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo)
if err != nil {
-
return nil, err
}
var branchesResp types.RepoBranchesResponse
if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil {
-
return nil, err
}
// if no ref specified, use default branch or first available
···
defer wg.Done()
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
if err != nil {
-
errs = errors.Join(errs, err)
return
}
if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil {
-
errs = errors.Join(errs, err)
}
}()
···
defer wg.Done()
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo)
if err != nil {
-
errs = errors.Join(errs, err)
return
}
treeResp = resp
···
defer wg.Done()
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo)
if err != nil {
-
errs = errors.Join(errs, err)
return
}
if err := json.Unmarshal(logBytes, &logResp); err != nil {
-
errs = errors.Join(errs, err)
}
}()
···
"fmt"
"log"
"net/http"
+
"net/url"
"slices"
"sort"
"strings"
···
func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
f, err := rp.repoResolver.Resolve(r)
if err != nil {
···
// first get branches to determine the ref if not specified
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo)
if err != nil {
+
return nil, fmt.Errorf("failed to call repoBranches: %w", err)
}
var branchesResp types.RepoBranchesResponse
if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil {
+
return nil, fmt.Errorf("failed to unmarshal branches response: %w", err)
}
// if no ref specified, use default branch or first available
···
defer wg.Done()
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
if err != nil {
+
errs = errors.Join(errs, fmt.Errorf("failed to call repoTags: %w", err))
return
}
if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil {
+
errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoTags: %w", err))
}
}()
···
defer wg.Done()
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo)
if err != nil {
+
errs = errors.Join(errs, fmt.Errorf("failed to call repoTree: %w", err))
return
}
treeResp = resp
···
defer wg.Done()
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo)
if err != nil {
+
errs = errors.Join(errs, fmt.Errorf("failed to call repoLog: %w", err))
return
}
if err := json.Unmarshal(logBytes, &logResp); err != nil {
+
errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoLog: %w", err))
}
}()
+97 -118
appview/repo/repo.go
···
}
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
-
refParam := chi.URLParam(r, "ref")
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
···
}
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
-
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", refParam, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.archive", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
rp.pages.Error404(w)
return
}
-
// Set headers for file download
-
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, refParam)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
···
}
ref := chi.URLParam(r, "ref")
scheme := "http"
if !rp.config.Core.Dev {
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.log", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
rp.pages.Error404(w)
return
}
···
}
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.tags", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
}
tagMap := make(map[string][]string)
···
}
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.branches", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
}
if branchBytes != nil {
···
return
}
ref := chi.URLParam(r, "ref")
var diffOpts types.DiffOpts
if d := r.URL.Query().Get("diff"); d == "split" {
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.diff", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
rp.pages.Error404(w)
return
}
···
}
ref := chi.URLParam(r, "ref")
-
treePath := chi.URLParam(r, "*")
// if the tree path has a trailing slash, let's strip it
// so we don't 404
treePath = strings.TrimSuffix(treePath, "/")
scheme := "http"
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.tree", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
rp.pages.Error404(w)
return
}
···
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
// so we can safely redirect to the "parent" (which is the same file).
-
unescapedTreePath, _ := url.PathUnescape(treePath)
-
if len(result.Files) == 0 && result.Parent == unescapedTreePath {
-
http.Redirect(w, r, fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound)
return
}
user := rp.oauth.GetUser(r)
var breadcrumbs [][]string
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
if treePath != "" {
for idx, elem := range strings.Split(treePath, "/") {
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
}
}
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.tags", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
rp.pages.Error404(w)
return
}
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.branches", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
rp.pages.Error404(w)
return
}
···
}
ref := chi.URLParam(r, "ref")
filePath := chi.URLParam(r, "*")
scheme := "http"
if !rp.config.Core.Dev {
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.blob", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
rp.pages.Error404(w)
return
}
// Use XRPC response directly instead of converting to internal types
var breadcrumbs [][]string
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
if filePath != "" {
for idx, elem := range strings.Split(filePath, "/") {
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], elem)})
}
}
···
// fetch the raw binary content using sh.tangled.repo.blob xrpc
repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
-
blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true",
-
scheme, f.Knot, url.QueryEscape(repoName), url.QueryEscape(ref), url.QueryEscape(filePath))
contentSrc = blobURL
if !rp.config.Core.Dev {
···
}
ref := chi.URLParam(r, "ref")
filePath := chi.URLParam(r, "*")
scheme := "http"
if !rp.config.Core.Dev {
···
}
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
-
blobURL := fmt.Sprintf("%s://%s/xrpc/sh.tangled.repo.blob?repo=%s&ref=%s&path=%s&raw=true",
-
scheme, f.Knot, url.QueryEscape(repo), url.QueryEscape(ref), url.QueryEscape(filePath))
req, err := http.NewRequest("GET", blobURL, nil)
if err != nil {
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.branches", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
rp.pages.Error503(w)
return
}
···
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
ref := chi.URLParam(r, "ref")
user := rp.oauth.GetUser(r)
f, err := rp.repoResolver.Resolve(r)
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.branches", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
return
}
···
}
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.tags", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
return
}
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.branches", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
return
}
···
}
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.tags", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
return
}
···
}
compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head)
-
if err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.compare", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
return
}
···
}
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
+
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
···
}
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.archive", xrpcerr)
+
rp.pages.Error503(w)
return
}
+
// Set headers for file download, just pass along whatever the knot specifies
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
+
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
···
}
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
scheme := "http"
if !rp.config.Core.Dev {
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.log", xrpcerr)
+
rp.pages.Error503(w)
return
}
···
}
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
+
rp.pages.Error503(w)
+
return
}
tagMap := make(map[string][]string)
···
}
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
+
rp.pages.Error503(w)
+
return
}
if branchBytes != nil {
···
return
}
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
var diffOpts types.DiffOpts
if d := r.URL.Query().Get("diff"); d == "split" {
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.diff", xrpcerr)
+
rp.pages.Error503(w)
return
}
···
}
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
// if the tree path has a trailing slash, let's strip it
// so we don't 404
+
treePath := chi.URLParam(r, "*")
+
treePath, _ = url.PathUnescape(treePath)
treePath = strings.TrimSuffix(treePath, "/")
scheme := "http"
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.tree", xrpcerr)
+
rp.pages.Error503(w)
return
}
···
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
// so we can safely redirect to the "parent" (which is the same file).
+
if len(result.Files) == 0 && result.Parent == treePath {
+
redirectTo := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), url.PathEscape(ref), result.Parent)
+
http.Redirect(w, r, redirectTo, http.StatusFound)
return
}
user := rp.oauth.GetUser(r)
var breadcrumbs [][]string
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
if treePath != "" {
for idx, elem := range strings.Split(treePath, "/") {
+
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
}
}
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
+
rp.pages.Error503(w)
return
}
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
+
rp.pages.Error503(w)
return
}
···
}
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
+
filePath := chi.URLParam(r, "*")
+
filePath, _ = url.PathUnescape(filePath)
scheme := "http"
if !rp.config.Core.Dev {
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.blob", xrpcerr)
+
rp.pages.Error503(w)
return
}
// Use XRPC response directly instead of converting to internal types
var breadcrumbs [][]string
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
if filePath != "" {
for idx, elem := range strings.Split(filePath, "/") {
+
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
}
}
···
// fetch the raw binary content using sh.tangled.repo.blob xrpc
repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
+
baseURL := &url.URL{
+
Scheme: scheme,
+
Host: f.Knot,
+
Path: "/xrpc/sh.tangled.repo.blob",
+
}
+
query := baseURL.Query()
+
query.Set("repo", repoName)
+
query.Set("ref", ref)
+
query.Set("path", filePath)
+
query.Set("raw", "true")
+
baseURL.RawQuery = query.Encode()
+
blobURL := baseURL.String()
contentSrc = blobURL
if !rp.config.Core.Dev {
···
}
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
+
filePath := chi.URLParam(r, "*")
+
filePath, _ = url.PathUnescape(filePath)
scheme := "http"
if !rp.config.Core.Dev {
···
}
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
+
baseURL := &url.URL{
+
Scheme: scheme,
+
Host: f.Knot,
+
Path: "/xrpc/sh.tangled.repo.blob",
+
}
+
query := baseURL.Query()
+
query.Set("repo", repo)
+
query.Set("ref", ref)
+
query.Set("path", filePath)
+
query.Set("raw", "true")
+
baseURL.RawQuery = query.Encode()
+
blobURL := baseURL.String()
req, err := http.NewRequest("GET", blobURL, nil)
if err != nil {
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
rp.pages.Error503(w)
return
}
···
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
user := rp.oauth.GetUser(r)
f, err := rp.repoResolver.Resolve(r)
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
+
rp.pages.Error503(w)
return
}
···
}
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
+
rp.pages.Error503(w)
return
}
···
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
+
rp.pages.Error503(w)
return
}
···
}
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
+
rp.pages.Error503(w)
return
}
···
}
compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.compare", xrpcerr)
+
rp.pages.Error503(w)
return
}
+7 -3
knotserver/xrpc/repo_archive.go
···
)
func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) {
-
repo, repoPath, unescapedRef, err := x.parseStandardParams(r)
if err != nil {
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
return
}
format := r.URL.Query().Get("format")
if format == "" {
format = "tar.gz" // default
···
return
}
-
gr, err := git.Open(repoPath, unescapedRef)
if err != nil {
writeError(w, xrpcerr.NewXrpcError(
xrpcerr.WithTag("RefNotFound"),
···
repoParts := strings.Split(repo, "/")
repoName := repoParts[len(repoParts)-1]
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-")
var archivePrefix string
if prefix != "" {
···
)
func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) {
+
repo := r.URL.Query().Get("repo")
+
repoPath, err := x.parseRepoParam(repo)
if err != nil {
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
return
}
+
ref := r.URL.Query().Get("ref")
+
// ref can be empty (git.Open handles this)
+
format := r.URL.Query().Get("format")
if format == "" {
format = "tar.gz" // default
···
return
}
+
gr, err := git.Open(repoPath, ref)
if err != nil {
writeError(w, xrpcerr.NewXrpcError(
xrpcerr.WithTag("RefNotFound"),
···
repoParts := strings.Split(repo, "/")
repoName := repoParts[len(repoParts)-1]
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
var archivePrefix string
if prefix != "" {
+5 -1
knotserver/xrpc/repo_blob.go
···
)
func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) {
-
_, repoPath, ref, err := x.parseStandardParams(r)
if err != nil {
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
return
}
treePath := r.URL.Query().Get("path")
if treePath == "" {
writeError(w, xrpcerr.NewXrpcError(
···
)
func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) {
+
repo := r.URL.Query().Get("repo")
+
repoPath, err := x.parseRepoParam(repo)
if err != nil {
writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
return
}
+
ref := r.URL.Query().Get("ref")
+
// ref can be empty (git.Open handles this)
+
treePath := r.URL.Query().Get("path")
if treePath == "" {
writeError(w, xrpcerr.NewXrpcError(
+3 -2
knotserver/xrpc/repo_branch.go
···
"encoding/json"
"net/http"
"net/url"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/knotserver/git"
···
Name: ref.Name().Short(),
Hash: ref.Hash().String(),
ShortHash: &[]string{ref.Hash().String()[:7]}[0],
-
When: commit.Author.When.Format("2006-01-02T15:04:05.000Z"),
IsDefault: &isDefault,
}
···
response.Author = &tangled.RepoBranch_Signature{
Name: commit.Author.Name,
Email: commit.Author.Email,
-
When: commit.Author.When.Format("2006-01-02T15:04:05.000Z"),
}
w.Header().Set("Content-Type", "application/json")
···
"encoding/json"
"net/http"
"net/url"
+
"time"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/knotserver/git"
···
Name: ref.Name().Short(),
Hash: ref.Hash().String(),
ShortHash: &[]string{ref.Hash().String()[:7]}[0],
+
When: commit.Author.When.Format(time.RFC3339),
IsDefault: &isDefault,
}
···
response.Author = &tangled.RepoBranch_Signature{
Name: commit.Author.Name,
Email: commit.Author.Email,
+
When: commit.Author.When.Format(time.RFC3339),
}
w.Header().Set("Content-Type", "application/json")
+1 -4
knotserver/xrpc/repo_branches.go
···
}
}
-
end := offset + limit
-
if end > len(branches) {
-
end = len(branches)
-
}
paginatedBranches := branches[offset:end]
···
}
}
+
end := min(offset+limit, len(branches))
paginatedBranches := branches[offset:end]
+4 -8
knotserver/xrpc/repo_compare.go
···
"encoding/json"
"fmt"
"net/http"
-
"net/url"
"tangled.sh/tangled.sh/core/knotserver/git"
"tangled.sh/tangled.sh/core/types"
···
return
}
-
rev1Param := r.URL.Query().Get("rev1")
-
if rev1Param == "" {
writeError(w, xrpcerr.NewXrpcError(
xrpcerr.WithTag("InvalidRequest"),
xrpcerr.WithMessage("missing rev1 parameter"),
···
return
}
-
rev2Param := r.URL.Query().Get("rev2")
-
if rev2Param == "" {
writeError(w, xrpcerr.NewXrpcError(
xrpcerr.WithTag("InvalidRequest"),
xrpcerr.WithMessage("missing rev2 parameter"),
···
return
}
-
rev1, _ := url.PathUnescape(rev1Param)
-
rev2, _ := url.PathUnescape(rev2Param)
-
gr, err := git.PlainOpen(repoPath)
if err != nil {
writeError(w, xrpcerr.NewXrpcError(
···
"encoding/json"
"fmt"
"net/http"
"tangled.sh/tangled.sh/core/knotserver/git"
"tangled.sh/tangled.sh/core/types"
···
return
}
+
rev1 := r.URL.Query().Get("rev1")
+
if rev1 == "" {
writeError(w, xrpcerr.NewXrpcError(
xrpcerr.WithTag("InvalidRequest"),
xrpcerr.WithMessage("missing rev1 parameter"),
···
return
}
+
rev2 := r.URL.Query().Get("rev2")
+
if rev2 == "" {
writeError(w, xrpcerr.NewXrpcError(
xrpcerr.WithTag("InvalidRequest"),
xrpcerr.WithMessage("missing rev2 parameter"),
···
return
}
gr, err := git.PlainOpen(repoPath)
if err != nil {
writeError(w, xrpcerr.NewXrpcError(
+2 -11
knotserver/xrpc/repo_diff.go
···
import (
"encoding/json"
"net/http"
-
"net/url"
"tangled.sh/tangled.sh/core/knotserver/git"
"tangled.sh/tangled.sh/core/types"
···
return
}
-
refParam := r.URL.Query().Get("ref")
-
if refParam == "" {
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InvalidRequest"),
-
xrpcerr.WithMessage("missing ref parameter"),
-
), http.StatusBadRequest)
-
return
-
}
-
-
ref, _ := url.QueryUnescape(refParam)
gr, err := git.Open(repoPath, ref)
if err != nil {
···
import (
"encoding/json"
"net/http"
"tangled.sh/tangled.sh/core/knotserver/git"
"tangled.sh/tangled.sh/core/types"
···
return
}
+
ref := r.URL.Query().Get("ref")
+
// ref can be empty (git.Open handles this)
gr, err := git.Open(repoPath, ref)
if err != nil {
+3 -9
knotserver/xrpc/repo_get_default_branch.go
···
import (
"encoding/json"
"net/http"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/knotserver/git"
···
return
}
-
gr, err := git.Open(repoPath, "")
-
if err != nil {
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("RepoNotFound"),
-
xrpcerr.WithMessage("repository not found"),
-
), http.StatusNotFound)
-
return
-
}
branch, err := gr.FindMainBranch()
if err != nil {
···
response := tangled.RepoGetDefaultBranch_Output{
Name: branch,
Hash: "",
-
When: "1970-01-01T00:00:00.000Z",
}
w.Header().Set("Content-Type", "application/json")
···
import (
"encoding/json"
"net/http"
+
"time"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/knotserver/git"
···
return
}
+
gr, err := git.PlainOpen(repoPath)
branch, err := gr.FindMainBranch()
if err != nil {
···
response := tangled.RepoGetDefaultBranch_Output{
Name: branch,
Hash: "",
+
When: time.UnixMicro(0).Format(time.RFC3339),
}
w.Header().Set("Content-Type", "application/json")
+2 -7
knotserver/xrpc/repo_languages.go
···
"encoding/json"
"math"
"net/http"
-
"net/url"
"time"
"tangled.sh/tangled.sh/core/api/tangled"
···
)
func (x *Xrpc) RepoLanguages(w http.ResponseWriter, r *http.Request) {
-
refParam := r.URL.Query().Get("ref")
-
if refParam == "" {
-
refParam = "HEAD" // default
-
}
-
ref, _ := url.PathUnescape(refParam)
-
repo := r.URL.Query().Get("repo")
repoPath, err := x.parseRepoParam(repo)
if err != nil {
···
return
}
gr, err := git.Open(repoPath, ref)
if err != nil {
x.Logger.Error("opening repo", "error", err.Error())
···
"encoding/json"
"math"
"net/http"
"time"
"tangled.sh/tangled.sh/core/api/tangled"
···
)
func (x *Xrpc) RepoLanguages(w http.ResponseWriter, r *http.Request) {
repo := r.URL.Query().Get("repo")
repoPath, err := x.parseRepoParam(repo)
if err != nil {
···
return
}
+
ref := r.URL.Query().Get("ref")
+
gr, err := git.Open(repoPath, ref)
if err != nil {
x.Logger.Error("opening repo", "error", err.Error())
+1 -18
knotserver/xrpc/repo_log.go
···
import (
"encoding/json"
"net/http"
-
"net/url"
"strconv"
"tangled.sh/tangled.sh/core/knotserver/git"
···
return
}
-
refParam := r.URL.Query().Get("ref")
-
if refParam == "" {
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InvalidRequest"),
-
xrpcerr.WithMessage("missing ref parameter"),
-
), http.StatusBadRequest)
-
return
-
}
path := r.URL.Query().Get("path")
cursor := r.URL.Query().Get("cursor")
···
}
}
-
ref, err := url.QueryUnescape(refParam)
-
if err != nil {
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InvalidRequest"),
-
xrpcerr.WithMessage("invalid ref parameter"),
-
), http.StatusBadRequest)
-
return
-
}
-
gr, err := git.Open(repoPath, ref)
if err != nil {
writeError(w, xrpcerr.NewXrpcError(
···
import (
"encoding/json"
"net/http"
"strconv"
"tangled.sh/tangled.sh/core/knotserver/git"
···
return
}
+
ref := r.URL.Query().Get("ref")
path := r.URL.Query().Get("path")
cursor := r.URL.Query().Get("cursor")
···
}
}
gr, err := git.Open(repoPath, ref)
if err != nil {
writeError(w, xrpcerr.NewXrpcError(
+2 -2
knotserver/xrpc/repo_tags.go
···
}
}
-
gr, err := git.Open(repoPath, "")
if err != nil {
x.Logger.Error("failed to open", "error", err)
writeError(w, xrpcerr.NewXrpcError(
xrpcerr.WithTag("RepoNotFound"),
xrpcerr.WithMessage("repository not found"),
-
), http.StatusNotFound)
return
}
···
}
}
+
gr, err := git.PlainOpen(repoPath)
if err != nil {
x.Logger.Error("failed to open", "error", err)
writeError(w, xrpcerr.NewXrpcError(
xrpcerr.WithTag("RepoNotFound"),
xrpcerr.WithMessage("repository not found"),
+
), http.StatusNoContent)
return
}
+4 -19
knotserver/xrpc/repo_tree.go
···
import (
"encoding/json"
"net/http"
-
"net/url"
"path/filepath"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/knotserver/git"
···
return
}
-
refParam := r.URL.Query().Get("ref")
-
if refParam == "" {
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InvalidRequest"),
-
xrpcerr.WithMessage("missing ref parameter"),
-
), http.StatusBadRequest)
-
return
-
}
path := r.URL.Query().Get("path")
// path can be empty (defaults to root)
-
ref, err := url.QueryUnescape(refParam)
-
if err != nil {
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InvalidRequest"),
-
xrpcerr.WithMessage("invalid ref parameter"),
-
), http.StatusBadRequest)
-
return
-
}
-
gr, err := git.Open(repoPath, ref)
if err != nil {
x.Logger.Error("failed to open git repository", "error", err, "path", repoPath, "ref", ref)
···
entry.Last_commit = &tangled.RepoTree_LastCommit{
Hash: file.LastCommit.Hash.String(),
Message: file.LastCommit.Message,
-
When: file.LastCommit.When.Format("2006-01-02T15:04:05.000Z"),
}
}
···
import (
"encoding/json"
"net/http"
"path/filepath"
+
"time"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/knotserver/git"
···
return
}
+
ref := r.URL.Query().Get("ref")
+
// ref can be empty (git.Open handles this)
path := r.URL.Query().Get("path")
// path can be empty (defaults to root)
gr, err := git.Open(repoPath, ref)
if err != nil {
x.Logger.Error("failed to open git repository", "error", err, "path", repoPath, "ref", ref)
···
entry.Last_commit = &tangled.RepoTree_LastCommit{
Hash: file.LastCommit.Hash.String(),
Message: file.LastCommit.Message,
+
When: file.LastCommit.When.Format(time.RFC3339),
}
}
+4 -27
knotserver/xrpc/xrpc.go
···
"encoding/json"
"log/slog"
"net/http"
-
"net/url"
"strings"
securejoin "github.com/cyphar/filepath-securejoin"
···
}
// Parse repo string (did/repoName format)
-
parts := strings.Split(repo, "/")
-
if len(parts) < 2 {
return "", xrpcerr.NewXrpcError(
xrpcerr.WithTag("InvalidRequest"),
xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"),
)
}
-
did := strings.Join(parts[:len(parts)-1], "/")
-
repoName := parts[len(parts)-1]
// Construct repository path using the same logic as didPath
didRepoPath, err := securejoin.SecureJoin(did, repoName)
···
return repoPath, nil
}
-
// parseStandardParams parses common query parameters used by most handlers
-
func (x *Xrpc) parseStandardParams(r *http.Request) (repo, repoPath, ref string, err error) {
-
// Parse repo parameter
-
repo = r.URL.Query().Get("repo")
-
repoPath, err = x.parseRepoParam(repo)
-
if err != nil {
-
return "", "", "", err
-
}
-
-
// Parse and unescape ref parameter
-
refParam := r.URL.Query().Get("ref")
-
if refParam == "" {
-
return "", "", "", xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InvalidRequest"),
-
xrpcerr.WithMessage("missing ref parameter"),
-
)
-
}
-
-
ref, _ = url.QueryUnescape(refParam)
-
return repo, repoPath, ref, nil
-
}
-
func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
···
"encoding/json"
"log/slog"
"net/http"
"strings"
securejoin "github.com/cyphar/filepath-securejoin"
···
}
// Parse repo string (did/repoName format)
+
parts := strings.SplitN(repo, "/", 2)
+
if len(parts) != 2 {
return "", xrpcerr.NewXrpcError(
xrpcerr.WithTag("InvalidRequest"),
xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"),
)
}
+
did := parts[0]
+
repoName := parts[1]
// Construct repository path using the same logic as didPath
didRepoPath, err := securejoin.SecureJoin(did, repoName)
···
return repoPath, nil
}
func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)