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

Compare changes

Choose any two refs to compare.

-157
appview/db/issues.go
···
return issues, nil
}
-
func GetIssuesWithLimit(e Execer, limit int, filters ...filter) ([]Issue, error) {
-
issues := make([]Issue, 0, limit)
-
-
var conditions []string
-
var args []any
-
for _, filter := range filters {
-
conditions = append(conditions, filter.Condition())
-
args = append(args, filter.Arg()...)
-
}
-
-
whereClause := ""
-
if conditions != nil {
-
whereClause = " where " + strings.Join(conditions, " and ")
-
}
-
limitClause := ""
-
if limit != 0 {
-
limitClause = fmt.Sprintf(" limit %d ", limit)
-
}
-
-
query := fmt.Sprintf(
-
`select
-
i.id,
-
i.owner_did,
-
i.repo_at,
-
i.issue_id,
-
i.created,
-
i.title,
-
i.body,
-
i.open
-
from
-
issues i
-
%s
-
order by
-
i.created desc
-
%s`,
-
whereClause, limitClause)
-
-
rows, err := e.Query(query, args...)
-
if err != nil {
-
return nil, err
-
}
-
defer rows.Close()
-
-
for rows.Next() {
-
var issue Issue
-
var issueCreatedAt string
-
err := rows.Scan(
-
&issue.Id,
-
&issue.Did,
-
&issue.RepoAt,
-
&issue.IssueId,
-
&issueCreatedAt,
-
&issue.Title,
-
&issue.Body,
-
&issue.Open,
-
)
-
if err != nil {
-
return nil, err
-
}
-
-
issueCreatedTime, err := time.Parse(time.RFC3339, issueCreatedAt)
-
if err != nil {
-
return nil, err
-
}
-
issue.Created = issueCreatedTime
-
-
issues = append(issues, issue)
-
}
-
-
if err := rows.Err(); err != nil {
-
return nil, err
-
}
-
-
return issues, nil
-
}
-
func GetIssues(e Execer, filters ...filter) ([]Issue, error) {
return GetIssuesPaginated(e, pagination.FirstPage(), filters...)
-
}
-
-
// timeframe here is directly passed into the sql query filter, and any
-
// timeframe in the past should be negative; e.g.: "-3 months"
-
func GetIssuesByOwnerDid(e Execer, ownerDid string, timeframe string) ([]Issue, error) {
-
var issues []Issue
-
-
rows, err := e.Query(
-
`select
-
i.id,
-
i.owner_did,
-
i.rkey,
-
i.repo_at,
-
i.issue_id,
-
i.created,
-
i.title,
-
i.body,
-
i.open,
-
r.did,
-
r.name,
-
r.knot,
-
r.rkey,
-
r.created
-
from
-
issues i
-
join
-
repos r on i.repo_at = r.at_uri
-
where
-
i.owner_did = ? and i.created >= date ('now', ?)
-
order by
-
i.created desc`,
-
ownerDid, timeframe)
-
if err != nil {
-
return nil, err
-
}
-
defer rows.Close()
-
-
for rows.Next() {
-
var issue Issue
-
var issueCreatedAt, repoCreatedAt string
-
var repo Repo
-
err := rows.Scan(
-
&issue.Id,
-
&issue.Did,
-
&issue.Rkey,
-
&issue.RepoAt,
-
&issue.IssueId,
-
&issueCreatedAt,
-
&issue.Title,
-
&issue.Body,
-
&issue.Open,
-
&repo.Did,
-
&repo.Name,
-
&repo.Knot,
-
&repo.Rkey,
-
&repoCreatedAt,
-
)
-
if err != nil {
-
return nil, err
-
}
-
-
issueCreatedTime, err := time.Parse(time.RFC3339, issueCreatedAt)
-
if err != nil {
-
return nil, err
-
}
-
issue.Created = issueCreatedTime
-
-
repoCreatedTime, err := time.Parse(time.RFC3339, repoCreatedAt)
-
if err != nil {
-
return nil, err
-
}
-
repo.Created = repoCreatedTime
-
-
issues = append(issues, issue)
-
}
-
-
if err := rows.Err(); err != nil {
-
return nil, err
-
}
-
-
return issues, nil
}
func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) {
+7 -3
appview/db/profile.go
···
*items = append(*items, &pull)
}
-
issues, err := GetIssuesByOwnerDid(e, forDid, timeframe)
+
issues, err := GetIssues(
+
e,
+
FilterEq("did", forDid),
+
FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
+
)
if err != nil {
return nil, fmt.Errorf("error getting issues by owner did: %w", err)
}
···
query = `select count(id) from pulls where owner_did = ? and state = ?`
args = append(args, did, PullOpen)
case VanityStatOpenIssueCount:
-
query = `select count(id) from issues where owner_did = ? and open = 1`
+
query = `select count(id) from issues where did = ? and open = 1`
args = append(args, did)
case VanityStatClosedIssueCount:
-
query = `select count(id) from issues where owner_did = ? and open = 0`
+
query = `select count(id) from issues where did = ? and open = 0`
args = append(args, did)
case VanityStatRepositoryCount:
query = `select count(id) from repos where did = ?`
+1 -1
appview/pages/markup/markdown.go
···
repoName := fmt.Sprintf("%s/%s", rctx.RepoInfo.OwnerDid, rctx.RepoInfo.Name)
query := fmt.Sprintf("repo=%s&ref=%s&path=%s&raw=true",
-
repoName, url.PathEscape(rctx.RepoInfo.Ref), actualPath)
+
url.PathEscape(repoName), url.PathEscape(rctx.RepoInfo.Ref), actualPath)
parsedURL := &url.URL{
Scheme: scheme,
+1 -1
appview/pages/templates/banner.html
···
<div class="mx-6">
These services may not be fully accessible until upgraded.
<a class="underline text-red-800 dark:text-red-200"
-
href="https://tangled.sh/@tangled.sh/core/tree/master/docs/migrations/">
+
href="https://tangled.sh/@tangled.sh/core/tree/master/docs/migrations.md">
Click to read the upgrade guide</a>.
</div>
</details>
+8
appview/pages/templates/fragments/logotype.html
···
+
{{ define "fragments/logotype" }}
+
<span class="flex items-center gap-2">
+
<span class="font-bold italic">tangled</span>
+
<span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1">
+
alpha
+
</span>
+
<span>
+
{{ end }}
+3 -6
appview/pages/templates/knots/index.html
···
{{ define "title" }}knots{{ end }}
{{ define "content" }}
-
<div class="px-6 py-4 flex items-end justify-start gap-4 align-bottom">
+
<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
<h1 class="text-xl font-bold dark:text-white">Knots</h1>
-
-
<span class="flex items-center gap-1 text-sm">
+
<span class="flex items-center gap-1">
{{ i "book" "w-3 h-3" }}
-
<a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/knot-hosting.md">
-
docs
-
</a>
+
<a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/knot-hosting.md">docs</a>
</span>
</div>
+4 -4
appview/pages/templates/layouts/base.html
···
<title>{{ block "title" . }}{{ end }} ยท tangled</title>
{{ block "extrameta" . }}{{ end }}
</head>
-
<body class="min-h-screen grid grid-cols-1 grid-rows-[min-content_auto_min-content] md:grid-cols-12 gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200">
+
<body class="min-h-screen grid grid-cols-1 grid-rows-[min-content_auto_min-content] md:grid-cols-10 lg:grid-cols-12 gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200">
{{ block "topbarLayout" . }}
-
<header class="px-1 col-span-1 md:col-start-3 md:col-span-8" style="z-index: 20;">
+
<header class="px-1 col-span-1 md:col-start-2 md:col-span-8 lg:col-start-3" style="z-index: 20;">
{{ if .LoggedInUser }}
<div id="upgrade-banner"
···
{{ end }}
{{ block "mainLayout" . }}
-
<div class="px-1 col-span-1 md:col-start-3 md:col-span-8 flex flex-col gap-4">
+
<div class="px-1 col-span-1 md:col-start-2 md:col-span-8 lg:col-start-3 flex flex-col gap-4">
{{ block "contentLayout" . }}
<main class="col-span-1 md:col-span-8">
{{ block "content" . }}{{ end }}
···
{{ end }}
{{ block "footerLayout" . }}
-
<footer class="px-1 col-span-1 md:col-start-3 md:col-span-8 mt-12">
+
<footer class="px-1 col-span-1 md:col-start-2 md:col-span-8 lg:col-start-3 mt-12">
{{ template "layouts/fragments/footer" . }}
</footer>
{{ end }}
+1 -3
appview/pages/templates/layouts/fragments/topbar.html
···
<nav class="space-x-4 px-6 py-2 rounded bg-white dark:bg-gray-800 dark:text-white drop-shadow-sm">
<div class="flex justify-between p-0 items-center">
<div id="left-items">
-
<a href="/" hx-boost="true" class="flex gap-2 font-bold italic">
-
tangled<sub>alpha</sub>
-
</a>
+
<a href="/" hx-boost="true" class="text-lg">{{ template "fragments/logotype" }}</a>
</div>
<div id="right-items" class="flex items-center gap-2">
+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> /
+
<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>
+
<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" (urlquery $.Ref) $.TreePath .Name }}
+
{{ $link := printf "/%s/%s/%s/%s/%s" $.RepoInfo.FullName "tree" (pathEscape $.Ref) $.TreePath .Name }}
{{ $icon := "folder" }}
{{ $iconStyle := "size-4 fill-current" }}
+3 -7
appview/pages/templates/spindles/index.html
···
{{ define "title" }}spindles{{ end }}
{{ define "content" }}
-
<div class="px-6 py-4 flex items-end justify-start gap-4 align-bottom">
+
<div class="px-6 py-4 flex items-center justify-between gap-4 align-bottom">
<h1 class="text-xl font-bold dark:text-white">Spindles</h1>
-
-
-
<span class="flex items-center gap-1 text-sm">
+
<span class="flex items-center gap-1">
{{ i "book" "w-3 h-3" }}
-
<a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/hosting.md">
-
docs
-
</a>
+
<a href="https://tangled.sh/@tangled.sh/core/blob/master/docs/spindle/hosting.md">docs</a>
</span>
</div>
+1 -1
appview/pages/templates/timeline/fragments/hero.html
···
<figure class="w-full hidden md:block md:w-auto">
<a href="https://tangled.sh/@tangled.sh/core" class="block">
-
<img src="https://assets.tangled.network/hero-repo.png" alt="Screenshot of the Tangled monorepo." class="max-w-md mx-auto md:max-w-none w-full md:w-[30vw] h-auto shadow-sm rounded hover:shadow-md transition-shadow" />
+
<img src="https://assets.tangled.network/hero-repo.png" alt="Screenshot of the Tangled monorepo." class="max-w-md mx-auto md:max-w-none w-full md:w-[30vw] h-auto shadow-sm rounded" />
</a>
<figcaption class="text-sm text-gray-600 dark:text-gray-400 mt-2 text-center">
Monorepo for Tangled, built in the open with the community.
+3 -3
appview/pages/templates/timeline/home.html
···
{{ define "feature" }}
{{ $info := index . 0 }}
{{ $bullets := index . 1 }}
-
<div class="flex flex-col items-top gap-6 md:flex-row md:gap-12">
+
<div class="flex flex-col items-center gap-6 md:flex-row md:items-top">
<div class="flex-1">
<h2 class="text-2xl font-bold text-black dark:text-white mb-6">{{ $info.title }}</h2>
<ul class="leading-normal">
···
</div>
<div class="flex-shrink-0 w-96 md:w-1/3">
<a href="{{ $info.image }}">
-
<img src="{{ $info.image }}" alt="{{ $info.alt }}" class="w-full h-auto rounded" />
+
<img src="{{ $info.image }}" alt="{{ $info.alt }}" class="w-full h-auto rounded shadow-sm" />
</a>
</div>
</div>
{{ end }}
{{ define "features" }}
-
<div class="prose dark:text-gray-200 space-y-12 px-6 py-4">
+
<div class="prose dark:text-gray-200 space-y-12 px-6 py-4 bg-white dark:bg-gray-800 rounded drop-shadow-sm">
{{ template "feature" (list
(dict
"title" "lightweight git repo hosting"
+2 -4
appview/pages/templates/user/completeSignup.html
···
</head>
<body class="flex items-center justify-center min-h-screen">
<main class="max-w-md px-6 -mt-4">
-
<h1
-
class="text-center text-2xl font-semibold italic dark:text-white"
-
>
-
tangled
+
<h1 class="flex place-content-center text-2xl font-semibold italic dark:text-white" >
+
{{ template "fragments/logotype" }}
</h1>
<h2 class="text-center text-xl italic dark:text-white">
tightly-knit social coding.
+2 -2
appview/pages/templates/user/login.html
···
</head>
<body class="flex items-center justify-center min-h-screen">
<main class="max-w-md px-6 -mt-4">
-
<h1 class="text-center text-2xl font-semibold italic dark:text-white" >
-
tangled
+
<h1 class="flex place-content-center text-2xl font-semibold italic dark:text-white" >
+
{{ template "fragments/logotype" }}
</h1>
<h2 class="text-center text-xl italic dark:text-white">
tightly-knit social coding.
+2 -2
appview/pages/templates/user/overview.html
···
</summary>
<div class="py-2 text-sm flex flex-col gap-3 mb-2">
{{ range $items }}
-
{{ $repoOwner := resolve .Metadata.Repo.Did }}
-
{{ $repoName := .Metadata.Repo.Name }}
+
{{ $repoOwner := resolve .Repo.Did }}
+
{{ $repoName := .Repo.Name }}
{{ $repoUrl := printf "%s/%s" $repoOwner $repoName }}
<div class="flex gap-2 text-gray-600 dark:text-gray-300">
+3 -1
appview/pages/templates/user/signup.html
···
</head>
<body class="flex items-center justify-center min-h-screen">
<main class="max-w-md px-6 -mt-4">
-
<h1 class="text-center text-2xl font-semibold italic dark:text-white" >tangled</h1>
+
<h1 class="flex place-content-center text-2xl font-semibold italic dark:text-white" >
+
{{ template "fragments/logotype" }}
+
</h1>
<h2 class="text-center text-xl italic dark:text-white">tightly-knit social coding.</h2>
<form
class="mt-4 max-w-sm mx-auto"
+6 -1
appview/repo/feed.go
···
"time"
"tangled.sh/tangled.sh/core/appview/db"
+
"tangled.sh/tangled.sh/core/appview/pagination"
"tangled.sh/tangled.sh/core/appview/reporesolver"
"github.com/bluesky-social/indigo/atproto/syntax"
···
return nil, err
}
-
issues, err := db.GetIssuesWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", f.RepoAt()))
+
issues, err := db.GetIssuesPaginated(
+
rp.db,
+
pagination.Page{Limit: feedLimitPerType},
+
db.FilterEq("repo_at", f.RepoAt()),
+
)
if err != nil {
return nil, err
}
+16 -17
appview/repo/index.go
···
"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 {
···
RepoInfo: repoInfo,
})
return
-
} else {
-
rp.pages.Error503(w)
-
log.Println("failed to build index response", err)
-
return
}
+
+
rp.pages.Error503(w)
+
log.Println("failed to build index response", err)
+
return
}
tagMap := make(map[string][]string)
···
// first get branches to determine the ref if not specified
branchesBytes, err := tangled.RepoBranches(ctx, xrpcc, "", 0, repo)
if err != nil {
-
return nil, err
+
return nil, fmt.Errorf("failed to call repoBranches: %w", err)
}
var branchesResp types.RepoBranchesResponse
if err := json.Unmarshal(branchesBytes, &branchesResp); err != nil {
-
return nil, err
+
return nil, fmt.Errorf("failed to unmarshal branches response: %w", err)
}
// if no ref specified, use default branch or first available
-
if ref == "" && len(branchesResp.Branches) > 0 {
+
if ref == "" {
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 {
+
// if ref is still empty, this means the default branch is not set
+
if ref == "" {
return &types.RepoIndexResponse{
IsEmpty: true,
Branches: branchesResp.Branches,
···
defer wg.Done()
tagsBytes, err := tangled.RepoTags(ctx, xrpcc, "", 0, repo)
if err != nil {
-
errs = errors.Join(errs, err)
+
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, err)
+
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, err)
+
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, err)
+
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, err)
+
errs = errors.Join(errs, fmt.Errorf("failed to unmarshal repoLog: %w", err))
}
}()
+98 -120
appview/repo/repo.go
···
"log/slog"
"net/http"
"net/url"
-
"path"
"path/filepath"
"slices"
"strconv"
···
}
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
-
refParam := chi.URLParam(r, "ref")
+
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", "", 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)
+
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
-
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, refParam)
+
// 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 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)
+
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 err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.tags", xrpcerr)
-
rp.pages.Error503(w)
-
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)
···
}
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 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 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)
+
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")
-
treePath := chi.URLParam(r, "*")
+
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 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)
+
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).
-
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)
+
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(), ref)})
+
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], elem)})
+
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 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)
+
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 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)
+
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 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)
+
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(), ref)})
+
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], elem)})
+
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 := 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))
+
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)
-
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))
+
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 err != nil {
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
log.Println("failed to call XRPC repo.branches", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
+
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 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.")
+
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 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.")
+
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 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.")
+
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 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.")
+
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 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.")
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
log.Println("failed to call XRPC repo.compare", xrpcerr)
+
rp.pages.Error503(w)
return
+10 -9
appview/state/profile.go
···
"github.com/gorilla/feeds"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview/db"
-
// "tangled.sh/tangled.sh/core/appview/oauth"
"tangled.sh/tangled.sh/core/appview/pages"
)
···
l = l.With("profileDid", profile.UserDid, "profileHandle", profile.UserHandle)
loggedInUser := s.oauth.GetUser(r)
+
params := FollowsPageParams{
+
Card: profile,
+
}
follows, err := fetchFollows(s.db, profile.UserDid)
if err != nil {
l.Error("failed to fetch follows", "err", err)
-
return nil, err
+
return &params, err
}
if len(follows) == 0 {
-
return nil, nil
+
return &params, nil
}
followDids := make([]string, 0, len(follows))
···
profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids))
if err != nil {
l.Error("failed to get profiles", "followDids", followDids, "err", err)
-
return nil, err
+
return &params, err
}
followStatsMap, err := db.GetFollowerFollowingCounts(s.db, followDids)
···
following, err := db.GetFollowing(s.db, loggedInUser.Did)
if err != nil {
l.Error("failed to get follow list", "err", err, "loggedInUser", loggedInUser.Did)
-
return nil, err
+
return &params, err
}
loggedInUserFollowing = make(map[string]struct{}, len(following))
for _, follow := range following {
···
}
}
-
return &FollowsPageParams{
-
Follows: followCards,
-
Card: profile,
-
}, nil
+
params.Follows = followCards
+
+
return &params, nil
}
func (s *State) followersPage(w http.ResponseWriter, r *http.Request) {
-35
docs/migrations/knot-1.7.0.md
···
-
# Upgrading from v1.7.0
-
-
After v1.7.0, knot secrets have been deprecated. You no
-
longer need a secret from the appview to run a knot. All
-
authorized commands to knots are managed via [Inter-Service
-
Authentication](https://atproto.com/specs/xrpc#inter-service-authentication-jwt).
-
Knots will be read-only until upgraded.
-
-
Upgrading is quite easy, in essence:
-
-
- `KNOT_SERVER_SECRET` is no more, you can remove this
-
environment variable entirely
-
- `KNOT_SERVER_OWNER` is now required on boot, set this to
-
your DID. You can find your DID in the
-
[settings](https://tangled.sh/settings) page.
-
- Restart your knot once you have replaced the environment
-
variable
-
- Head to the [knot dashboard](https://tangled.sh/knots) and
-
hit the "retry" button to verify your knot. This simply
-
writes a `sh.tangled.knot` record to your PDS.
-
-
## Nix
-
-
If you use the nix module, simply bump the flake to the
-
latest revision, and change your config block like so:
-
-
```diff
-
services.tangled-knot = {
-
enable = true;
-
server = {
-
- secretFile = /path/to/secret;
-
+ owner = "did:plc:foo";
-
};
-
};
-
```
+60
docs/migrations.md
···
+
# Migrations
+
+
This document is laid out in reverse-chronological order.
+
Newer migration guides are listed first, and older guides
+
are further down the page.
+
+
## Upgrading from v1.8.x
+
+
After v1.8.2, the HTTP API for knot and spindles have been
+
deprecated and replaced with XRPC. Repositories on outdated
+
knots will not be viewable from the appview. Upgrading is
+
straightforward however.
+
+
For knots:
+
+
- Upgrade to latest tag (v1.9.0 or above)
+
- Head to the [knot dashboard](https://tangled.sh/knots) and
+
hit the "retry" button to verify your knot
+
+
For spindles:
+
+
- Upgrade to latest tag (v1.9.0 or above)
+
- Head to the [spindle
+
dashboard](https://tangled.sh/spindles) and hit the
+
"retry" button to verify your spindle
+
+
## Upgrading from v1.7.x
+
+
After v1.7.0, knot secrets have been deprecated. You no
+
longer need a secret from the appview to run a knot. All
+
authorized commands to knots are managed via [Inter-Service
+
Authentication](https://atproto.com/specs/xrpc#inter-service-authentication-jwt).
+
Knots will be read-only until upgraded.
+
+
Upgrading is quite easy, in essence:
+
+
- `KNOT_SERVER_SECRET` is no more, you can remove this
+
environment variable entirely
+
- `KNOT_SERVER_OWNER` is now required on boot, set this to
+
your DID. You can find your DID in the
+
[settings](https://tangled.sh/settings) page.
+
- Restart your knot once you have replaced the environment
+
variable
+
- Head to the [knot dashboard](https://tangled.sh/knots) and
+
hit the "retry" button to verify your knot. This simply
+
writes a `sh.tangled.knot` record to your PDS.
+
+
If you use the nix module, simply bump the flake to the
+
latest revision, and change your config block like so:
+
+
```diff
+
services.tangled-knot = {
+
enable = true;
+
server = {
+
- secretFile = /path/to/secret;
+
+ owner = "did:plc:foo";
+
};
+
};
+
```
+
+1 -10
knotserver/xrpc/list_keys.go
···
package xrpc
import (
-
"encoding/json"
"net/http"
"strconv"
···
response.Cursor = &nextCursor
}
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(response); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
+1 -10
knotserver/xrpc/owner.go
···
package xrpc
import (
-
"encoding/json"
"net/http"
"tangled.sh/tangled.sh/core/api/tangled"
···
Owner: owner,
}
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(response); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
+8 -7
knotserver/xrpc/repo_archive.go
···
)
func (x *Xrpc) RepoArchive(w http.ResponseWriter, r *http.Request) {
-
repo, repoPath, unescapedRef, err := x.parseStandardParams(r)
+
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 == "" {
···
return
}
-
gr, err := git.Open(repoPath, unescapedRef)
+
gr, err := git.Open(repoPath, ref)
if err != nil {
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("RefNotFound"),
-
xrpcerr.WithMessage("repository or ref not found"),
-
), http.StatusNotFound)
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
return
}
repoParts := strings.Split(repo, "/")
repoName := repoParts[len(repoParts)-1]
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(unescapedRef).Short(), "/", "-")
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
var archivePrefix string
if prefix != "" {
+8 -15
knotserver/xrpc/repo_blob.go
···
import (
"crypto/sha256"
"encoding/base64"
-
"encoding/json"
"fmt"
"net/http"
"path/filepath"
···
)
func (x *Xrpc) RepoBlob(w http.ResponseWriter, r *http.Request) {
-
_, repoPath, ref, err := x.parseStandardParams(r)
+
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(
···
gr, err := git.Open(repoPath, ref)
if err != nil {
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("RefNotFound"),
-
xrpcerr.WithMessage("repository or ref not found"),
-
), http.StatusNotFound)
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
return
}
···
return
}
w.Header().Set("ETag", eTag)
+
w.Header().Set("Content-Type", mimeType)
case strings.HasPrefix(mimeType, "text/"):
w.Header().Set("Cache-Control", "public, no-cache")
···
response.MimeType = &mimeType
}
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(response); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
// isTextualMimeType returns true if the MIME type represents textual content
+5 -16
knotserver/xrpc/repo_branch.go
···
package xrpc
import (
-
"encoding/json"
"net/http"
"net/url"
+
"time"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/knotserver/git"
···
gr, err := git.PlainOpen(repoPath)
if err != nil {
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("RepoNotFound"),
-
xrpcerr.WithMessage("repository not found"),
-
), http.StatusNotFound)
+
writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent)
return
}
···
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"),
+
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("2006-01-02T15:04:05.000Z"),
+
When: commit.Author.When.Format(time.RFC3339),
}
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(response); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
+11 -25
knotserver/xrpc/repo_branches.go
···
package xrpc
import (
-
"encoding/json"
"net/http"
"strconv"
···
cursor := r.URL.Query().Get("cursor")
-
limit := 50 // default
-
if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
-
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
-
limit = l
-
}
-
}
+
// limit := 50 // default
+
// if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
+
// if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
+
// limit = l
+
// }
+
// }
+
+
limit := 500
gr, err := git.PlainOpen(repoPath)
if err != nil {
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("RepoNotFound"),
-
xrpcerr.WithMessage("repository not found"),
-
), http.StatusNotFound)
+
writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent)
return
}
···
}
}
-
end := offset + limit
-
if end > len(branches) {
-
end = len(branches)
-
}
+
end := min(offset+limit, len(branches))
paginatedBranches := branches[offset:end]
···
Branches: paginatedBranches,
}
-
// Write JSON response directly
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(response); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
+7 -23
knotserver/xrpc/repo_compare.go
···
package xrpc
import (
-
"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 == "" {
+
rev1 := r.URL.Query().Get("rev1")
+
if rev1 == "" {
writeError(w, xrpcerr.NewXrpcError(
xrpcerr.WithTag("InvalidRequest"),
xrpcerr.WithMessage("missing rev1 parameter"),
···
return
}
-
rev2Param := r.URL.Query().Get("rev2")
-
if rev2Param == "" {
+
rev2 := r.URL.Query().Get("rev2")
+
if rev2 == "" {
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(
-
xrpcerr.WithTag("RepoNotFound"),
-
xrpcerr.WithMessage("repository not found"),
-
), http.StatusNotFound)
+
writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent)
return
}
···
return
}
-
resp := types.RepoFormatPatchResponse{
+
response := types.RepoFormatPatchResponse{
Rev1: commit1.Hash.String(),
Rev2: commit2.Hash.String(),
FormatPatch: formatPatch,
Patch: rawPatch,
}
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(resp); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
+6 -30
knotserver/xrpc/repo_diff.go
···
package xrpc
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)
+
ref := r.URL.Query().Get("ref")
+
// ref can be empty (git.Open handles this)
gr, err := git.Open(repoPath, ref)
if err != nil {
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("RefNotFound"),
-
xrpcerr.WithMessage("repository or ref not found"),
-
), http.StatusNotFound)
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
return
}
diff, err := gr.Diff()
if err != nil {
x.Logger.Error("getting diff", "error", err.Error())
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("RefNotFound"),
-
xrpcerr.WithMessage("failed to generate diff"),
-
), http.StatusInternalServerError)
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusInternalServerError)
return
}
-
resp := types.RepoCommitResponse{
+
response := types.RepoCommitResponse{
Ref: ref,
Diff: diff,
}
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(resp); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
+4 -19
knotserver/xrpc/repo_get_default_branch.go
···
package xrpc
import (
-
"encoding/json"
"net/http"
+
"time"
"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
-
}
+
gr, err := git.PlainOpen(repoPath)
branch, err := gr.FindMainBranch()
if err != nil {
···
response := tangled.RepoGetDefaultBranch_Output{
Name: branch,
Hash: "",
-
When: "1970-01-01T00:00:00.000Z",
+
When: time.UnixMicro(0).Format(time.RFC3339),
}
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(response); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
+4 -21
knotserver/xrpc/repo_languages.go
···
import (
"context"
-
"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
}
+
ref := r.URL.Query().Get("ref")
+
gr, err := git.Open(repoPath, ref)
if err != nil {
x.Logger.Error("opening repo", "error", err.Error())
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("RefNotFound"),
-
xrpcerr.WithMessage("repository or ref not found"),
-
), http.StatusNotFound)
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
return
}
···
response.TotalFiles = &totalFiles
}
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(response); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
+14 -34
knotserver/xrpc/repo_log.go
···
package xrpc
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
-
}
+
ref := r.URL.Query().Get("ref")
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(
-
xrpcerr.WithTag("RefNotFound"),
-
xrpcerr.WithMessage("repository or ref not found"),
-
), http.StatusNotFound)
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
return
}
···
return
}
+
total, err := gr.TotalCommits()
+
if err != nil {
+
x.Logger.Error("fetching total commits", "error", err.Error())
+
writeError(w, xrpcerr.NewXrpcError(
+
xrpcerr.WithTag("InternalServerError"),
+
xrpcerr.WithMessage("failed to fetch total commits"),
+
), http.StatusNotFound)
+
return
+
}
+
// Create response using existing types.RepoLogResponse
response := types.RepoLogResponse{
Commits: commits,
Ref: ref,
Page: (offset / limit) + 1,
PerPage: limit,
-
Total: len(commits), // This is not accurate for pagination, but matches existing behavior
+
Total: total,
}
if path != "" {
···
response.Log = true
-
// Write JSON response directly
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(response); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
+3 -16
knotserver/xrpc/repo_tags.go
···
package xrpc
import (
-
"encoding/json"
"net/http"
"strconv"
···
}
}
-
gr, err := git.Open(repoPath, "")
+
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.StatusNotFound)
+
writeError(w, xrpcerr.RepoNotFoundError, http.StatusNoContent)
return
}
···
Tags: paginatedTags,
}
-
// Write JSON response directly
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(response); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
+6 -33
knotserver/xrpc/repo_tree.go
···
package xrpc
import (
-
"encoding/json"
"net/http"
-
"net/url"
"path/filepath"
+
"time"
"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
-
}
+
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)
-
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)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("RefNotFound"),
-
xrpcerr.WithMessage("repository or ref not found"),
-
), http.StatusNotFound)
+
writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
return
}
···
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"),
+
When: file.LastCommit.When.Format(time.RFC3339),
}
}
···
Files: treeEntries,
}
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(response); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
+1 -11
knotserver/xrpc/version.go
···
package xrpc
import (
-
"encoding/json"
"fmt"
"net/http"
"runtime/debug"
"tangled.sh/tangled.sh/core/api/tangled"
-
xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
)
// version is set during build time.
···
Version: version,
}
-
w.Header().Set("Content-Type", "application/json")
-
if err := json.NewEncoder(w).Encode(response); err != nil {
-
x.Logger.Error("failed to encode response", "error", err)
-
writeError(w, xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("InternalServerError"),
-
xrpcerr.WithMessage("failed to encode response"),
-
), http.StatusInternalServerError)
-
return
-
}
+
writeJson(w, response)
}
+14 -35
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 {
+
parts := strings.SplitN(repo, "/", 2)
+
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]
+
did := parts[0]
+
repoName := parts[1]
// Construct repository path using the same logic as didPath
didRepoPath, err := securejoin.SecureJoin(did, repoName)
if err != nil {
-
return "", xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("RepoNotFound"),
-
xrpcerr.WithMessage("failed to access repository"),
-
)
+
return "", xrpcerr.RepoNotFoundError
}
repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath)
if err != nil {
-
return "", xrpcerr.NewXrpcError(
-
xrpcerr.WithTag("RepoNotFound"),
-
xrpcerr.WithMessage("failed to access repository"),
-
)
+
return "", xrpcerr.RepoNotFoundError
}
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)
json.NewEncoder(w).Encode(e)
}
+
+
func writeJson(w http.ResponseWriter, response any) {
+
w.Header().Set("Content-Type", "application/json")
+
if err := json.NewEncoder(w).Encode(response); err != nil {
+
writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
+
return
+
}
+
}
+8 -2
nix/gomod2nix.toml
···
[mod."github.com/whyrusleeping/cbor-gen"]
version = "v0.3.1"
hash = "sha256-PAd8M2Z8t6rVRBII+Rg8Bz+QaJIwbW64bfyqsv31kgc="
+
[mod."github.com/wyatt915/goldmark-treeblood"]
+
version = "v0.0.0-20250825231212-5dcbdb2f4b57"
+
hash = "sha256-IZEsUXTBTsNgWoD7vqRUc9aFCCHNjzk1IUmI9O+NCnM="
+
[mod."github.com/wyatt915/treeblood"]
+
version = "v0.1.15"
+
hash = "sha256-hb99exdkoY2Qv8WdDxhwgPXGbEYimUr6wFtPXEvcO9g="
[mod."github.com/yuin/goldmark"]
-
version = "v1.4.15"
-
hash = "sha256-MvSOT6dwf5hVYkIg4MnqMpsy5ZtWZ7amAE7Zo9HkEa0="
+
version = "v1.7.12"
+
hash = "sha256-thLYBS4woL2X5qRdo7vP+xCvjlGRDU0jXtDCUt6vvWM="
[mod."github.com/yuin/goldmark-highlighting/v2"]
version = "v2.0.0-20230729083705-37449abec8cc"
hash = "sha256-HpiwU7jIeDUAg2zOpTIiviQir8dpRPuXYh2nqFFccpg="
+15 -17
nix/pkgs/knot-unwrapped.nix
···
modules,
sqlite-lib,
src,
-
}:
-
let
-
version = "1.8.1-alpha";
+
}: let
+
version = "1.9.0-alpha";
in
-
buildGoApplication {
-
pname = "knot";
-
version = "1.8.1";
-
inherit src modules;
+
buildGoApplication {
+
pname = "knot";
+
inherit src version modules;
-
doCheck = false;
+
doCheck = false;
-
subPackages = ["cmd/knot"];
-
tags = ["libsqlite3"];
+
subPackages = ["cmd/knot"];
+
tags = ["libsqlite3"];
-
ldflags = [
-
"-X tangled.sh/tangled.sh/core/knotserver/xrpc.version=${version}"
-
];
+
ldflags = [
+
"-X tangled.sh/tangled.sh/core/knotserver/xrpc.version=${version}"
+
];
-
env.CGO_CFLAGS = "-I ${sqlite-lib}/include ";
-
env.CGO_LDFLAGS = "-L ${sqlite-lib}/lib";
-
CGO_ENABLED = 1;
-
}
+
env.CGO_CFLAGS = "-I ${sqlite-lib}/include ";
+
env.CGO_LDFLAGS = "-L ${sqlite-lib}/lib";
+
CGO_ENABLED = 1;
+
}
+10
xrpc/errors/errors.go
···
WithMessage("owner not set for this service"),
)
+
var RepoNotFoundError = NewXrpcError(
+
WithTag("RepoNotFound"),
+
WithMessage("failed to access repository"),
+
)
+
+
var RefNotFoundError = NewXrpcError(
+
WithTag("RefNotFound"),
+
WithMessage("failed to access ref"),
+
)
+
var AuthError = func(err error) XrpcError {
return NewXrpcError(
WithTag("Auth"),