diff --git a/appview/pages/markup/format.go b/appview/pages/markup/format.go
index 5b104c7d..ffa7b511 100644
--- a/appview/pages/markup/format.go
+++ b/appview/pages/markup/format.go
@@ -13,6 +13,18 @@ var FileTypes map[Format][]string = map[Format][]string{
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 {
diff --git a/appview/pages/pages.go b/appview/pages/pages.go
index 9afd120e..ee62bd02 100644
--- a/appview/pages/pages.go
+++ b/appview/pages/pages.go
@@ -763,7 +763,12 @@ type RepoBlobParams struct {
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 {
diff --git a/appview/pages/templates/repo/fragments/diff.html b/appview/pages/templates/repo/fragments/diff.html
index 81d15b9b..1d6ceefd 100644
--- a/appview/pages/templates/repo/fragments/diff.html
+++ b/appview/pages/templates/repo/fragments/diff.html
@@ -11,6 +11,11 @@
{{ $last := sub (len $diff) 1 }}
+ {{ if eq (len $diff) 0 }}
+
+
No differences found between the selected revisions.
+
+ {{ else }}
{{ range $idx, $hunk := $diff }}
{{ with $hunk }}
@@ -50,5 +55,6 @@
{{ end }}
{{ end }}
+ {{ end }}
{{ end }}
diff --git a/appview/repo/artifact.go b/appview/repo/artifact.go
index 151fd91a..a16dad7d 100644
--- a/appview/repo/artifact.go
+++ b/appview/repo/artifact.go
@@ -1,6 +1,8 @@
package repo
import (
+ "context"
+ "encoding/json"
"fmt"
"log"
"net/http"
@@ -9,6 +11,7 @@ import (
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"
@@ -17,7 +20,7 @@ import (
"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"
)
@@ -33,7 +36,7 @@ func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) {
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")
@@ -140,7 +143,7 @@ func (rp *Repo) DownloadArtifact(w http.ResponseWriter, r *http.Request) {
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")
@@ -259,23 +262,38 @@ func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) {
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
+ }
+
var tag *types.TagReference
for _, t := range result.Tags {
if t.Tag != nil {
diff --git a/appview/repo/index.go b/appview/repo/index.go
index 7b7c91bf..9a6f6045 100644
--- a/appview/repo/index.go
+++ b/appview/repo/index.go
@@ -1,17 +1,27 @@
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"
@@ -27,17 +37,20 @@ func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
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
}
@@ -102,7 +115,7 @@ func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
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
@@ -135,8 +148,9 @@ func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
}
func (rp *Repo) getLanguageInfo(
+ ctx context.Context,
f *reporesolver.ResolvedRepo,
- us *knotclient.UnsignedClient,
+ xrpcc *indigoxrpc.Client,
currentRef string,
isDefaultRef bool,
) ([]types.RepoLanguageDetails, error) {
@@ -148,22 +162,28 @@ func (rp *Repo) getLanguageInfo(
)
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,
})
}
@@ -206,3 +226,187 @@ func (rp *Repo) getLanguageInfo(
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
+}
diff --git a/appview/repo/repo.go b/appview/repo/repo.go
index 1ab0a600..012a8c2c 100644
--- a/appview/repo/repo.go
+++ b/appview/repo/repo.go
@@ -11,6 +11,7 @@ import (
"log/slog"
"net/http"
"net/url"
+ "path"
"path/filepath"
"slices"
"strconv"
@@ -19,6 +20,7 @@ import (
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"
@@ -31,7 +33,6 @@ import (
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"
@@ -92,15 +93,35 @@ func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
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,
}
- 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)
+ 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)))
+
+ // Write the archive data directly
+ w.Write(archiveBytes)
}
func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) {
@@ -120,55 +141,87 @@ 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)
}
@@ -176,7 +229,7 @@ func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) {
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)
@@ -189,7 +242,7 @@ func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) {
LoggedInUser: user,
TagMap: tagMap,
RepoInfo: repoInfo,
- RepoLogResponse: *repolog,
+ RepoLogResponse: xrpcResp,
EmailToDidOrHandle: emailToDidOrHandle(rp, emailToDidMap),
VerifiedCommits: vc,
Pipelines: pipelines,
@@ -301,10 +354,6 @@ func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) {
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" {
@@ -316,23 +365,31 @@ func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) {
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
}
@@ -378,41 +435,66 @@ func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) {
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,
@@ -451,16 +533,31 @@ func (rp *Repo) RepoTags(w http.ResponseWriter, r *http.Request) {
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
}
@@ -496,7 +593,7 @@ func (rp *Repo) RepoTags(w http.ResponseWriter, r *http.Request) {
rp.pages.RepoTags(w, pages.RepoTagsParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
- RepoTagsResponse: *result,
+ RepoTagsResponse: result,
ArtifactMap: artifactMap,
DanglingArtifacts: danglingArtifacts,
})
@@ -509,16 +606,31 @@ func (rp *Repo) RepoBranches(w http.ResponseWriter, r *http.Request) {
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
}
@@ -528,7 +640,7 @@ func (rp *Repo) RepoBranches(w http.ResponseWriter, r *http.Request) {
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
- RepoBranchesResponse: *result,
+ RepoBranchesResponse: result,
})
}
@@ -541,34 +653,29 @@ func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) {
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)})
@@ -581,7 +688,7 @@ func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) {
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"
}
@@ -591,8 +698,8 @@ func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) {
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
@@ -602,27 +709,52 @@ func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) {
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,
})
}
@@ -637,12 +769,14 @@ func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
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 {
@@ -685,17 +819,15 @@ func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
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
@@ -716,12 +848,7 @@ func isTextualMimeType(mimeType string) bool {
"message/",
}
- for _, t := range textualTypes {
- if mimeType == t {
- return true
- }
- }
- return false
+ return slices.Contains(textualTypes, mimeType)
}
// modify the spindle configured for this repo
@@ -1227,16 +1354,31 @@ func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) {
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
}
@@ -1607,20 +1749,34 @@ func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) {
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)
@@ -1644,10 +1800,21 @@ func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) {
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.")
+ 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.")
- log.Println("failed to reach knotserver", err)
return
}
@@ -1699,33 +1866,71 @@ func (rp *Repo) RepoCompare(w http.ResponseWriter, r *http.Request) {
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
}
- branches, err := us.Branches(f.OwnerDid(), f.Name)
- if err != nil {
+ 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.")
- log.Println("failed to reach knotserver", err)
return
}
- 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
}
- formatPatch, err := us.Compare(f.OwnerDid(), f.Name, base, head)
+ 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
+ }
+
+ 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 compare", err)
return
}
+
+ 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.")
+ return
+ }
+
diff := patchutil.AsNiceDiff(formatPatch.Patch, base)
repoinfo := f.RepoInfo(user)
diff --git a/appview/xrpcclient/xrpc.go b/appview/xrpcclient/xrpc.go
index eac8abe2..b0ad8377 100644
--- a/appview/xrpcclient/xrpc.go
+++ b/appview/xrpcclient/xrpc.go
@@ -115,7 +115,7 @@ func HandleXrpcErr(err error) error {
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 {