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

appview/repo: rework repo handlers to use xrpc calls

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

Changed files
+629 -179
appview
pages
markup
templates
repo
fragments
repo
xrpcclient
+12
appview/pages/markup/format.go
···
FormatMarkdown: []string{".md", ".markdown", ".mdown", ".mkdn", ".mkd"},
}
+
// ReadmeFilenames contains the list of common README filenames to search for,
+
// in order of preference. Only includes well-supported formats.
+
var ReadmeFilenames = []string{
+
"README.md", "readme.md",
+
"README",
+
"readme",
+
"README.markdown",
+
"readme.markdown",
+
"README.txt",
+
"readme.txt",
+
}
+
func GetFormat(filename string) Format {
for format, extensions := range FileTypes {
for _, extension := range extensions {
+6 -1
appview/pages/pages.go
···
ShowRendered bool
RenderToggle bool
RenderedContents template.HTML
-
types.RepoBlobResponse
+
*tangled.RepoBlob_Output
+
// Computed fields for template compatibility
+
Contents string
+
Lines int
+
SizeHint uint64
+
IsBinary bool
}
func (p *Pages) RepoBlob(w io.Writer, params RepoBlobParams) error {
+6
appview/pages/templates/repo/fragments/diff.html
···
{{ $last := sub (len $diff) 1 }}
<div class="flex flex-col gap-4">
+
{{ if eq (len $diff) 0 }}
+
<div class="text-center text-gray-500 dark:text-gray-400 py-8">
+
<p>No differences found between the selected revisions.</p>
+
</div>
+
{{ else }}
{{ range $idx, $hunk := $diff }}
{{ with $hunk }}
<details open id="file-{{ .Name.New }}" class="group border border-gray-200 dark:border-gray-700 w-full mx-auto rounded bg-white dark:bg-gray-800 drop-shadow-sm" tabindex="{{ add $idx 1 }}">
···
</div>
</details>
{{ end }}
+
{{ end }}
{{ end }}
</div>
{{ end }}
+26 -8
appview/repo/artifact.go
···
package repo
import (
+
"context"
+
"encoding/json"
"fmt"
"log"
"net/http"
···
comatproto "github.com/bluesky-social/indigo/api/atproto"
lexutil "github.com/bluesky-social/indigo/lex/util"
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
"github.com/dustin/go-humanize"
"github.com/go-chi/chi/v5"
"github.com/go-git/go-git/v5/plumbing"
···
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/appview/reporesolver"
-
"tangled.sh/tangled.sh/core/knotclient"
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
"tangled.sh/tangled.sh/core/tid"
"tangled.sh/tangled.sh/core/types"
)
···
return
}
-
tag, err := rp.resolveTag(f, tagParam)
+
tag, err := rp.resolveTag(r.Context(), f, tagParam)
if err != nil {
log.Println("failed to resolve tag", err)
rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
···
return
}
-
tag, err := rp.resolveTag(f, tagParam)
+
tag, err := rp.resolveTag(r.Context(), f, tagParam)
if err != nil {
log.Println("failed to resolve tag", err)
rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
···
w.Write([]byte{})
}
-
func (rp *Repo) resolveTag(f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) {
+
func (rp *Repo) resolveTag(ctx context.Context, f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) {
tagParam, err := url.QueryUnescape(tagParam)
if err != nil {
return nil, err
}
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
-
if err != nil {
-
return nil, err
+
scheme := "http"
+
if !rp.config.Core.Dev {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
}
-
result, err := us.Tags(f.OwnerDid(), f.Name)
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
xrpcBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
if err != nil {
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
+
return nil, xrpcerr
+
}
log.Println("failed to reach knotserver", err)
+
return nil, err
+
}
+
+
var result types.RepoTagsResponse
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
+
log.Println("failed to decode XRPC tags response", err)
return nil, err
}
+220 -16
appview/repo/index.go
···
package repo
import (
+
"fmt"
"log"
"net/http"
"slices"
"sort"
"strings"
+
"sync"
+
"time"
+
"context"
+
"encoding/json"
+
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+
"github.com/go-git/go-git/v5/plumbing"
+
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview/commitverify"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/pages"
+
"tangled.sh/tangled.sh/core/appview/pages/markup"
"tangled.sh/tangled.sh/core/appview/reporesolver"
-
"tangled.sh/tangled.sh/core/knotclient"
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
"tangled.sh/tangled.sh/core/types"
"github.com/go-chi/chi/v5"
···
return
}
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
-
if err != nil {
-
log.Printf("failed to create unsigned client for %s", f.Knot)
-
rp.pages.Error503(w)
-
return
+
scheme := "http"
+
if !rp.config.Core.Dev {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
}
-
result, err := us.Index(f.OwnerDid(), f.Name, ref)
+
// Build index response from multiple XRPC calls
+
result, err := rp.buildIndexResponse(r.Context(), xrpcc, f, ref)
if err != nil {
rp.pages.Error503(w)
-
log.Println("failed to reach knotserver", err)
+
log.Println("failed to build index response", err)
return
}
···
repoInfo := f.RepoInfo(user)
// TODO: a bit dirty
-
languageInfo, err := rp.getLanguageInfo(f, us, result.Ref, ref == "")
+
languageInfo, err := rp.getLanguageInfo(r.Context(), f, xrpcc, result.Ref, ref == "")
if err != nil {
log.Printf("failed to compute language percentages: %s", err)
// non-fatal
···
}
func (rp *Repo) getLanguageInfo(
+
ctx context.Context,
f *reporesolver.ResolvedRepo,
-
us *knotclient.UnsignedClient,
+
xrpcc *indigoxrpc.Client,
currentRef string,
isDefaultRef bool,
) ([]types.RepoLanguageDetails, error) {
···
)
if err != nil || langs == nil {
-
// non-fatal, fetch langs from ks
-
ls, err := us.RepoLanguages(f.OwnerDid(), f.Name, currentRef)
+
// non-fatal, fetch langs from ks via XRPC
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
ls, err := tangled.RepoLanguages(ctx, xrpcc, currentRef, repo)
if err != nil {
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.languages", xrpcerr)
+
return nil, xrpcerr
+
}
return nil, err
}
-
if ls == nil {
+
+
if ls == nil || ls.Languages == nil {
return nil, nil
}
-
for l, s := range ls.Languages {
+
for _, lang := range ls.Languages {
langs = append(langs, db.RepoLanguage{
RepoAt: f.RepoAt(),
Ref: currentRef,
IsDefaultRef: isDefaultRef,
-
Language: l,
-
Bytes: s,
+
Language: lang.Name,
+
Bytes: lang.Size,
})
}
···
return languageStats, nil
}
+
+
// buildIndexResponse creates a RepoIndexResponse by combining multiple xrpc calls in parallel
+
func (rp *Repo) buildIndexResponse(ctx context.Context, xrpcc *indigoxrpc.Client, f *reporesolver.ResolvedRepo, ref string) (*types.RepoIndexResponse, error) {
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
+
// first get branches to determine the ref if not specified
+
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo)
+
if err != nil {
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
+
return nil, xrpcerr
+
}
+
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
+
if ref == "" && len(branchesResp.Branches) > 0 {
+
for _, branch := range branchesResp.Branches {
+
if branch.IsDefault {
+
ref = branch.Name
+
break
+
}
+
}
+
if ref == "" {
+
ref = branchesResp.Branches[0].Name
+
}
+
}
+
+
// check if repo is empty
+
if len(branchesResp.Branches) == 0 {
+
return &types.RepoIndexResponse{
+
IsEmpty: true,
+
Branches: branchesResp.Branches,
+
}, nil
+
}
+
+
// now run the remaining queries in parallel
+
var wg sync.WaitGroup
+
var mu sync.Mutex
+
var errs []error
+
+
var (
+
tagsResp types.RepoTagsResponse
+
treeResp *tangled.RepoTree_Output
+
logResp types.RepoLogResponse
+
readmeContent string
+
readmeFileName string
+
)
+
+
// tags
+
wg.Add(1)
+
go func() {
+
defer wg.Done()
+
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
+
if err != nil {
+
mu.Lock()
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.tags", xrpcerr)
+
errs = append(errs, xrpcerr)
+
} else {
+
errs = append(errs, err)
+
}
+
mu.Unlock()
+
return
+
}
+
+
if err := json.Unmarshal(tagsBytes, &tagsResp); err != nil {
+
mu.Lock()
+
errs = append(errs, err)
+
mu.Unlock()
+
}
+
}()
+
+
// tree/files
+
wg.Add(1)
+
go func() {
+
defer wg.Done()
+
resp, err := tangled.RepoTree(ctx, xrpcc, "", ref, repo)
+
if err != nil {
+
mu.Lock()
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.tree", xrpcerr)
+
errs = append(errs, xrpcerr)
+
} else {
+
errs = append(errs, err)
+
}
+
mu.Unlock()
+
return
+
}
+
treeResp = resp
+
}()
+
+
// commits
+
wg.Add(1)
+
go func() {
+
defer wg.Done()
+
logBytes, err := tangled.RepoLog(ctx, xrpcc, "", 50, "", ref, repo)
+
if err != nil {
+
mu.Lock()
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.log", xrpcerr)
+
errs = append(errs, xrpcerr)
+
} else {
+
errs = append(errs, err)
+
}
+
mu.Unlock()
+
return
+
}
+
+
if err := json.Unmarshal(logBytes, &logResp); err != nil {
+
mu.Lock()
+
errs = append(errs, err)
+
mu.Unlock()
+
}
+
}()
+
+
// readme content
+
wg.Add(1)
+
go func() {
+
defer wg.Done()
+
for _, filename := range markup.ReadmeFilenames {
+
blobResp, err := tangled.RepoBlob(ctx, xrpcc, filename, false, ref, repo)
+
if err != nil {
+
continue
+
}
+
+
if blobResp == nil {
+
continue
+
}
+
+
readmeContent = blobResp.Content
+
readmeFileName = filename
+
break
+
}
+
}()
+
+
wg.Wait()
+
+
if len(errs) > 0 {
+
return nil, errs[0] // return first error
+
}
+
+
var files []types.NiceTree
+
if treeResp != nil && treeResp.Files != nil {
+
for _, file := range treeResp.Files {
+
niceFile := types.NiceTree{
+
IsFile: file.Is_file,
+
IsSubtree: file.Is_subtree,
+
Name: file.Name,
+
Mode: file.Mode,
+
Size: file.Size,
+
}
+
if file.Last_commit != nil {
+
when, _ := time.Parse(time.RFC3339, file.Last_commit.When)
+
niceFile.LastCommit = &types.LastCommitInfo{
+
Hash: plumbing.NewHash(file.Last_commit.Hash),
+
Message: file.Last_commit.Message,
+
When: when,
+
}
+
}
+
files = append(files, niceFile)
+
}
+
}
+
+
result := &types.RepoIndexResponse{
+
IsEmpty: false,
+
Ref: ref,
+
Readme: readmeContent,
+
ReadmeFileName: readmeFileName,
+
Commits: logResp.Commits,
+
Description: logResp.Description,
+
Files: files,
+
Branches: branchesResp.Branches,
+
Tags: tagsResp.Tags,
+
TotalCommits: logResp.Total,
+
}
+
+
return result, nil
+
}
+358 -153
appview/repo/repo.go
···
"log/slog"
"net/http"
"net/url"
+
"path"
"path/filepath"
"slices"
"strconv"
···
comatproto "github.com/bluesky-social/indigo/api/atproto"
lexutil "github.com/bluesky-social/indigo/lex/util"
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview/commitverify"
"tangled.sh/tangled.sh/core/appview/config"
···
xrpcclient "tangled.sh/tangled.sh/core/appview/xrpcclient"
"tangled.sh/tangled.sh/core/eventconsumer"
"tangled.sh/tangled.sh/core/idresolver"
-
"tangled.sh/tangled.sh/core/knotclient"
"tangled.sh/tangled.sh/core/patchutil"
"tangled.sh/tangled.sh/core/rbac"
"tangled.sh/tangled.sh/core/tid"
···
return
}
-
var uri string
-
if rp.config.Core.Dev {
-
uri = "http"
-
} else {
-
uri = "https"
+
scheme := "http"
+
if !rp.config.Core.Dev {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
+
}
+
+
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
}
-
url := fmt.Sprintf("%s://%s/%s/%s/archive/%s.tar.gz", uri, f.Knot, f.OwnerDid(), f.Name, url.PathEscape(refParam))
-
http.Redirect(w, r, url, http.StatusFound)
+
// 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)))
+
+
// Write the archive data directly
+
w.Write(archiveBytes)
}
func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) {
···
ref := chi.URLParam(r, "ref")
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
+
scheme := "http"
+
if !rp.config.Core.Dev {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
+
}
+
+
limit := int64(60)
+
cursor := ""
+
if page > 1 {
+
// Convert page number to cursor (offset)
+
offset := (page - 1) * int(limit)
+
cursor = strconv.Itoa(offset)
+
}
+
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo)
if err != nil {
-
log.Println("failed to create unsigned client", err)
+
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
}
-
repolog, err := us.Log(f.OwnerDid(), f.Name, ref, page)
-
if err != nil {
+
var xrpcResp types.RepoLogResponse
+
if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil {
+
log.Println("failed to decode XRPC response", err)
rp.pages.Error503(w)
-
log.Println("failed to reach knotserver", err)
return
}
-
tagResult, err := us.Tags(f.OwnerDid(), f.Name)
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
if err != nil {
-
rp.pages.Error503(w)
-
log.Println("failed to reach knotserver", err)
-
return
+
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)
-
for _, tag := range tagResult.Tags {
-
hash := tag.Hash
-
if tag.Tag != nil {
-
hash = tag.Tag.Target.String()
+
if tagBytes != nil {
+
var tagResp types.RepoTagsResponse
+
if err := json.Unmarshal(tagBytes, &tagResp); err == nil {
+
for _, tag := range tagResp.Tags {
+
tagMap[tag.Hash] = append(tagMap[tag.Hash], tag.Name)
+
}
}
-
tagMap[hash] = append(tagMap[hash], tag.Name)
}
-
branchResult, err := us.Branches(f.OwnerDid(), f.Name)
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
if err != nil {
-
rp.pages.Error503(w)
-
log.Println("failed to reach knotserver", err)
-
return
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.branches", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
}
-
for _, branch := range branchResult.Branches {
-
hash := branch.Hash
-
tagMap[hash] = append(tagMap[hash], branch.Name)
+
if branchBytes != nil {
+
var branchResp types.RepoBranchesResponse
+
if err := json.Unmarshal(branchBytes, &branchResp); err == nil {
+
for _, branch := range branchResp.Branches {
+
tagMap[branch.Hash] = append(tagMap[branch.Hash], branch.Name)
+
}
+
}
}
user := rp.oauth.GetUser(r)
-
emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(repolog.Commits), true)
+
emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true)
if err != nil {
log.Println("failed to fetch email to did mapping", err)
}
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, repolog.Commits)
+
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits)
if err != nil {
log.Println(err)
}
···
repoInfo := f.RepoInfo(user)
var shas []string
-
for _, c := range repolog.Commits {
+
for _, c := range xrpcResp.Commits {
shas = append(shas, c.Hash.String())
}
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
···
LoggedInUser: user,
TagMap: tagMap,
RepoInfo: repoInfo,
-
RepoLogResponse: *repolog,
+
RepoLogResponse: xrpcResp,
EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
VerifiedCommits: vc,
Pipelines: pipelines,
···
return
}
ref := chi.URLParam(r, "ref")
-
protocol := "http"
-
if !rp.config.Core.Dev {
-
protocol = "https"
-
}
var diffOpts types.DiffOpts
if d := r.URL.Query().Get("diff"); d == "split" {
···
return
}
-
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref))
-
if err != nil {
-
rp.pages.Error503(w)
-
log.Println("failed to reach knotserver", err)
-
return
+
scheme := "http"
+
if !rp.config.Core.Dev {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
}
-
body, err := io.ReadAll(resp.Body)
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo)
if err != nil {
-
log.Printf("Error reading response body: %v", err)
+
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
}
var result types.RepoCommitResponse
-
err = json.Unmarshal(body, &result)
-
if err != nil {
-
log.Println("failed to parse response:", err)
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
+
log.Println("failed to decode XRPC response", err)
+
rp.pages.Error503(w)
return
}
···
ref := chi.URLParam(r, "ref")
treePath := chi.URLParam(r, "*")
-
protocol := "http"
-
if !rp.config.Core.Dev {
-
protocol = "https"
-
}
// if the tree path has a trailing slash, let's strip it
// so we don't 404
treePath = strings.TrimSuffix(treePath, "/")
-
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, treePath))
-
if err != nil {
-
rp.pages.Error503(w)
-
log.Println("failed to reach knotserver", err)
-
return
+
scheme := "http"
+
if !rp.config.Core.Dev {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
}
-
// uhhh so knotserver returns a 500 if the entry isn't found in
-
// the requested tree path, so let's stick to not-OK here.
-
// we can fix this once we build out the xrpc apis for these operations.
-
if resp.StatusCode != http.StatusOK {
+
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
}
-
body, err := io.ReadAll(resp.Body)
-
if err != nil {
-
log.Printf("Error reading response body: %v", err)
-
return
+
// Convert XRPC response to internal types.RepoTreeResponse
+
files := make([]types.NiceTree, len(xrpcResp.Files))
+
for i, xrpcFile := range xrpcResp.Files {
+
file := types.NiceTree{
+
Name: xrpcFile.Name,
+
Mode: xrpcFile.Mode,
+
Size: int64(xrpcFile.Size),
+
IsFile: xrpcFile.Is_file,
+
IsSubtree: xrpcFile.Is_subtree,
+
}
+
+
// Convert last commit info if present
+
if xrpcFile.Last_commit != nil {
+
commitWhen, _ := time.Parse(time.RFC3339, xrpcFile.Last_commit.When)
+
file.LastCommit = &types.LastCommitInfo{
+
Hash: plumbing.NewHash(xrpcFile.Last_commit.Hash),
+
Message: xrpcFile.Last_commit.Message,
+
When: commitWhen,
+
}
+
}
+
+
files[i] = file
}
-
var result types.RepoTreeResponse
-
err = json.Unmarshal(body, &result)
-
if err != nil {
-
log.Println("failed to parse response:", err)
-
return
+
result := types.RepoTreeResponse{
+
Ref: xrpcResp.Ref,
+
Files: files,
+
}
+
+
if xrpcResp.Parent != nil {
+
result.Parent = *xrpcResp.Parent
+
}
+
if xrpcResp.Dotdot != nil {
+
result.DotDot = *xrpcResp.Dotdot
}
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
···
return
}
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
+
scheme := "http"
+
if !rp.config.Core.Dev {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
+
}
+
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
if err != nil {
-
log.Println("failed to create unsigned client", err)
+
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
}
-
result, err := us.Tags(f.OwnerDid(), f.Name)
-
if err != nil {
+
var result types.RepoTagsResponse
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
+
log.Println("failed to decode XRPC response", err)
rp.pages.Error503(w)
-
log.Println("failed to reach knotserver", err)
return
}
···
rp.pages.RepoTags(w, pages.RepoTagsParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
-
RepoTagsResponse: *result,
+
RepoTagsResponse: result,
ArtifactMap: artifactMap,
DanglingArtifacts: danglingArtifacts,
})
···
return
}
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
+
scheme := "http"
+
if !rp.config.Core.Dev {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
+
}
+
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
if err != nil {
-
log.Println("failed to create unsigned client", err)
+
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
}
-
result, err := us.Branches(f.OwnerDid(), f.Name)
-
if err != nil {
+
var result types.RepoBranchesResponse
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
+
log.Println("failed to decode XRPC response", err)
rp.pages.Error503(w)
-
log.Println("failed to reach knotserver", err)
return
}
···
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
-
RepoBranchesResponse: *result,
+
RepoBranchesResponse: result,
})
}
···
ref := chi.URLParam(r, "ref")
filePath := chi.URLParam(r, "*")
-
protocol := "http"
+
+
scheme := "http"
if !rp.config.Core.Dev {
-
protocol = "https"
-
}
-
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath))
-
if err != nil {
-
rp.pages.Error503(w)
-
log.Println("failed to reach knotserver", err)
-
return
+
scheme = "https"
}
-
-
if resp.StatusCode == http.StatusNotFound {
-
rp.pages.Error404(w)
-
return
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
}
-
body, err := io.ReadAll(resp.Body)
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
+
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
if err != nil {
-
log.Printf("Error reading response body: %v", err)
+
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
}
-
var result types.RepoBlobResponse
-
err = json.Unmarshal(body, &result)
-
if err != nil {
-
log.Println("failed to parse response:", err)
-
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)})
···
showRendered := false
renderToggle := false
-
if markup.GetFormat(result.Path) == markup.FormatMarkdown {
+
if markup.GetFormat(resp.Path) == markup.FormatMarkdown {
renderToggle = true
showRendered = r.URL.Query().Get("code") != "true"
}
···
var isVideo bool
var contentSrc string
-
if result.IsBinary {
-
ext := strings.ToLower(filepath.Ext(result.Path))
+
if resp.IsBinary != nil && *resp.IsBinary {
+
ext := strings.ToLower(filepath.Ext(resp.Path))
switch ext {
case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp":
isImage = true
···
unsupported = true
}
-
// fetch the actual binary content like in RepoBlobRaw
+
// fetch the raw binary content using sh.tangled.repo.blob xrpc
+
repoName := path.Join("%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))
-
blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Name, ref, filePath)
contentSrc = blobURL
if !rp.config.Core.Dev {
contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL)
}
}
+
lines := 0
+
if resp.IsBinary == nil || !*resp.IsBinary {
+
lines = strings.Count(resp.Content, "\n") + 1
+
}
+
+
var sizeHint uint64
+
if resp.Size != nil {
+
sizeHint = uint64(*resp.Size)
+
} else {
+
sizeHint = uint64(len(resp.Content))
+
}
+
user := rp.oauth.GetUser(r)
+
+
// Determine if content is binary (dereference pointer)
+
isBinary := false
+
if resp.IsBinary != nil {
+
isBinary = *resp.IsBinary
+
}
+
rp.pages.RepoBlob(w, pages.RepoBlobParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
RepoBlobResponse: result,
-
BreadCrumbs: breadcrumbs,
-
ShowRendered: showRendered,
-
RenderToggle: renderToggle,
-
Unsupported: unsupported,
-
IsImage: isImage,
-
IsVideo: isVideo,
-
ContentSrc: contentSrc,
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
BreadCrumbs: breadcrumbs,
+
ShowRendered: showRendered,
+
RenderToggle: renderToggle,
+
Unsupported: unsupported,
+
IsImage: isImage,
+
IsVideo: isVideo,
+
ContentSrc: contentSrc,
+
RepoBlob_Output: resp,
+
Contents: resp.Content,
+
Lines: lines,
+
SizeHint: sizeHint,
+
IsBinary: isBinary,
})
}
···
ref := chi.URLParam(r, "ref")
filePath := chi.URLParam(r, "*")
-
protocol := "http"
+
scheme := "http"
if !rp.config.Core.Dev {
-
protocol = "https"
+
scheme = "https"
}
-
blobURL := fmt.Sprintf("%s://%s/%s/%s/raw/%s/%s", protocol, f.Knot, f.OwnerDid(), f.Repo.Name, ref, filePath)
+
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 {
···
return
}
-
// Safely serve content based on type
if strings.HasPrefix(contentType, "text/") || isTextualMimeType(contentType) {
-
// Serve all textual content as text/plain for security
+
// serve all textual content as text/plain
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Write(body)
} else if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") {
-
// Serve images and videos with their original content type
+
// serve images and videos with their original content type
w.Header().Set("Content-Type", contentType)
w.Write(body)
} else {
-
// Block potentially dangerous content types
w.WriteHeader(http.StatusUnsupportedMediaType)
w.Write([]byte("unsupported content type"))
return
···
"message/",
}
-
for _, t := range textualTypes {
-
if mimeType == t {
-
return true
-
}
-
}
-
return false
+
return slices.Contains(textualTypes, mimeType)
}
// modify the spindle configured for this repo
···
f, err := rp.repoResolver.Resolve(r)
user := rp.oauth.GetUser(r)
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
+
scheme := "http"
+
if !rp.config.Core.Dev {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
+
}
+
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
if err != nil {
-
log.Println("failed to create unsigned client", err)
+
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
-
result, err := us.Branches(f.OwnerDid(), f.Name)
-
if err != nil {
+
var result types.RepoBranchesResponse
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
+
log.Println("failed to decode XRPC response", err)
rp.pages.Error503(w)
-
log.Println("failed to reach knotserver", err)
return
···
return
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
+
scheme := "http"
+
if !rp.config.Core.Dev {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
+
}
+
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
if err != nil {
-
log.Printf("failed to create unsigned client for %s", f.Knot)
-
rp.pages.Error503(w)
+
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
-
result, err := us.Branches(f.OwnerDid(), f.Name)
-
if err != nil {
+
var branchResult types.RepoBranchesResponse
+
if err := json.Unmarshal(branchBytes, &branchResult); err != nil {
+
log.Println("failed to decode XRPC branches response", err)
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
-
log.Println("failed to reach knotserver", err)
return
-
branches := result.Branches
+
branches := branchResult.Branches
sortBranches(branches)
···
head = queryHead
-
tags, err := us.Tags(f.OwnerDid(), f.Name)
+
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.")
-
log.Println("failed to reach knotserver", err)
+
return
+
}
+
+
var tags types.RepoTagsResponse
+
if err := json.Unmarshal(tagBytes, &tags); err != nil {
+
log.Println("failed to decode XRPC tags response", err)
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
return
···
return
-
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
+
scheme := "http"
+
if !rp.config.Core.Dev {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
+
}
+
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
if err != nil {
-
log.Printf("failed to create unsigned client for %s", f.Knot)
-
rp.pages.Error503(w)
+
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
+
}
+
+
var branches types.RepoBranchesResponse
+
if err := json.Unmarshal(branchBytes, &branches); err != nil {
+
log.Println("failed to decode XRPC branches response", err)
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
return
-
branches, err := us.Branches(f.OwnerDid(), f.Name)
+
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.")
-
log.Println("failed to reach knotserver", err)
+
return
+
}
+
+
var tags types.RepoTagsResponse
+
if err := json.Unmarshal(tagBytes, &tags); err != nil {
+
log.Println("failed to decode XRPC tags response", err)
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
return
-
tags, err := us.Tags(f.OwnerDid(), f.Name)
+
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.")
-
log.Println("failed to reach knotserver", err)
return
-
formatPatch, err := us.Compare(f.OwnerDid(), f.Name, base, head)
-
if err != nil {
+
var formatPatch types.RepoFormatPatchResponse
+
if err := json.Unmarshal(compareBytes, &formatPatch); err != nil {
+
log.Println("failed to decode XRPC compare response", err)
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
-
log.Println("failed to compare", err)
return
+
diff := patchutil.AsNiceDiff(formatPatch.Patch, base)
repoinfo := f.RepoInfo(user)
+1 -1
appview/xrpcclient/xrpc.go
···
var xrpcerr *indigoxrpc.Error
if ok := errors.As(err, &xrpcerr); !ok {
-
return fmt.Errorf("Recieved invalid XRPC error response.")
+
return fmt.Errorf("Recieved invalid XRPC error response: %v", err)
}
switch xrpcerr.StatusCode {