appview/repo: split up handlers into separate files #763

merged
opened by oppi.li targeting master from push-qwwtnptuywsl
+49
appview/repo/archive.go
···
+
package repo
+
+
import (
+
"fmt"
+
"net/http"
+
"net/url"
+
"strings"
+
+
"tangled.org/core/api/tangled"
+
xrpcclient "tangled.org/core/appview/xrpcclient"
+
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+
"github.com/go-chi/chi/v5"
+
"github.com/go-git/go-git/v5/plumbing"
+
)
+
+
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "DownloadArchive")
+
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
scheme := "http"
+
if !rp.config.Core.Dev {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
+
}
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
// Set headers for file download, just pass along whatever the knot specifies
+
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
+
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename)
+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
+
w.Header().Set("Content-Type", "application/gzip")
+
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
+
// Write the archive data directly
+
w.Write(archiveBytes)
+
}
+219
appview/repo/blob.go
···
+
package repo
+
+
import (
+
"fmt"
+
"io"
+
"net/http"
+
"net/url"
+
"path/filepath"
+
"slices"
+
"strings"
+
+
"tangled.org/core/api/tangled"
+
"tangled.org/core/appview/pages"
+
"tangled.org/core/appview/pages/markup"
+
xrpcclient "tangled.org/core/appview/xrpcclient"
+
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+
"github.com/go-chi/chi/v5"
+
)
+
+
func (rp *Repo) Blob(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "RepoBlob")
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
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 {
+
scheme = "https"
+
}
+
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
+
xrpcc := &indigoxrpc.Client{
+
Host: host,
+
}
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
+
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.blob", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
// Use XRPC response directly instead of converting to internal types
+
var breadcrumbs [][]string
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
+
if filePath != "" {
+
for idx, elem := range strings.Split(filePath, "/") {
+
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
+
}
+
}
+
showRendered := false
+
renderToggle := false
+
if markup.GetFormat(resp.Path) == markup.FormatMarkdown {
+
renderToggle = true
+
showRendered = r.URL.Query().Get("code") != "true"
+
}
+
var unsupported bool
+
var isImage bool
+
var isVideo bool
+
var contentSrc string
+
if resp.IsBinary != nil && *resp.IsBinary {
+
ext := strings.ToLower(filepath.Ext(resp.Path))
+
switch ext {
+
case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp":
+
isImage = true
+
case ".mp4", ".webm", ".ogg", ".mov", ".avi":
+
isVideo = true
+
default:
+
unsupported = true
+
}
+
// fetch the raw binary content using sh.tangled.repo.blob xrpc
+
repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
+
baseURL := &url.URL{
+
Scheme: scheme,
+
Host: f.Knot,
+
Path: "/xrpc/sh.tangled.repo.blob",
+
}
+
query := baseURL.Query()
+
query.Set("repo", repoName)
+
query.Set("ref", ref)
+
query.Set("path", filePath)
+
query.Set("raw", "true")
+
baseURL.RawQuery = query.Encode()
+
blobURL := baseURL.String()
+
contentSrc = blobURL
+
if !rp.config.Core.Dev {
+
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),
+
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,
+
})
+
}
+
+
func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "RepoBlobRaw")
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
w.WriteHeader(http.StatusBadRequest)
+
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 {
+
scheme = "https"
+
}
+
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
+
baseURL := &url.URL{
+
Scheme: scheme,
+
Host: f.Knot,
+
Path: "/xrpc/sh.tangled.repo.blob",
+
}
+
query := baseURL.Query()
+
query.Set("repo", repo)
+
query.Set("ref", ref)
+
query.Set("path", filePath)
+
query.Set("raw", "true")
+
baseURL.RawQuery = query.Encode()
+
blobURL := baseURL.String()
+
req, err := http.NewRequest("GET", blobURL, nil)
+
if err != nil {
+
l.Error("failed to create request", "err", err)
+
return
+
}
+
// forward the If-None-Match header
+
if clientETag := r.Header.Get("If-None-Match"); clientETag != "" {
+
req.Header.Set("If-None-Match", clientETag)
+
}
+
client := &http.Client{}
+
resp, err := client.Do(req)
+
if err != nil {
+
l.Error("failed to reach knotserver", "err", err)
+
rp.pages.Error503(w)
+
return
+
}
+
defer resp.Body.Close()
+
// forward 304 not modified
+
if resp.StatusCode == http.StatusNotModified {
+
w.WriteHeader(http.StatusNotModified)
+
return
+
}
+
if resp.StatusCode != http.StatusOK {
+
l.Error("knotserver returned non-OK status for raw blob", "url", blobURL, "statuscode", resp.StatusCode)
+
w.WriteHeader(resp.StatusCode)
+
_, _ = io.Copy(w, resp.Body)
+
return
+
}
+
contentType := resp.Header.Get("Content-Type")
+
body, err := io.ReadAll(resp.Body)
+
if err != nil {
+
l.Error("error reading response body from knotserver", "err", err)
+
w.WriteHeader(http.StatusInternalServerError)
+
return
+
}
+
if strings.HasPrefix(contentType, "text/") || isTextualMimeType(contentType) {
+
// 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
+
w.Header().Set("Content-Type", contentType)
+
w.Write(body)
+
} else {
+
w.WriteHeader(http.StatusUnsupportedMediaType)
+
w.Write([]byte("unsupported content type"))
+
return
+
}
+
}
+
+
func isTextualMimeType(mimeType string) bool {
+
textualTypes := []string{
+
"application/json",
+
"application/xml",
+
"application/yaml",
+
"application/x-yaml",
+
"application/toml",
+
"application/javascript",
+
"application/ecmascript",
+
"message/",
+
}
+
return slices.Contains(textualTypes, mimeType)
+
}
+95
appview/repo/branches.go
···
+
package repo
+
+
import (
+
"encoding/json"
+
"fmt"
+
"net/http"
+
+
"tangled.org/core/api/tangled"
+
"tangled.org/core/appview/oauth"
+
"tangled.org/core/appview/pages"
+
xrpcclient "tangled.org/core/appview/xrpcclient"
+
"tangled.org/core/types"
+
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+
)
+
+
func (rp *Repo) Branches(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "RepoBranches")
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
var result types.RepoBranchesResponse
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
+
l.Error("failed to decode XRPC response", "err", err)
+
rp.pages.Error503(w)
+
return
+
}
+
sortBranches(result.Branches)
+
user := rp.oauth.GetUser(r)
+
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
RepoBranchesResponse: result,
+
})
+
}
+
+
func (rp *Repo) DeleteBranch(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "DeleteBranch")
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
noticeId := "delete-branch-error"
+
fail := func(msg string, err error) {
+
l.Error(msg, "err", err)
+
rp.pages.Notice(w, noticeId, msg)
+
}
+
branch := r.FormValue("branch")
+
if branch == "" {
+
fail("No branch provided.", nil)
+
return
+
}
+
client, err := rp.oauth.ServiceClient(
+
r,
+
oauth.WithService(f.Knot),
+
oauth.WithLxm(tangled.RepoDeleteBranchNSID),
+
oauth.WithDev(rp.config.Core.Dev),
+
)
+
if err != nil {
+
fail("Failed to connect to knotserver", nil)
+
return
+
}
+
err = tangled.RepoDeleteBranch(
+
r.Context(),
+
client,
+
&tangled.RepoDeleteBranch_Input{
+
Branch: branch,
+
Repo: f.RepoAt().String(),
+
},
+
)
+
if err := xrpcclient.HandleXrpcErr(err); err != nil {
+
fail(fmt.Sprintf("Failed to delete branch: %s", err), err)
+
return
+
}
+
l.Error("deleted branch from knot", "branch", branch, "repo", f.RepoAt())
+
rp.pages.HxRefresh(w)
+
}
+214
appview/repo/compare.go
···
+
package repo
+
+
import (
+
"encoding/json"
+
"fmt"
+
"net/http"
+
"net/url"
+
"strings"
+
+
"tangled.org/core/api/tangled"
+
"tangled.org/core/appview/pages"
+
xrpcclient "tangled.org/core/appview/xrpcclient"
+
"tangled.org/core/patchutil"
+
"tangled.org/core/types"
+
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+
"github.com/go-chi/chi/v5"
+
)
+
+
func (rp *Repo) CompareNew(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "RepoCompareNew")
+
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
+
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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
+
var branchResult types.RepoBranchesResponse
+
if err := json.Unmarshal(branchBytes, &branchResult); err != nil {
+
l.Error("failed to decode XRPC branches response", "err", err)
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
+
return
+
}
+
branches := branchResult.Branches
+
+
sortBranches(branches)
+
+
var defaultBranch string
+
for _, b := range branches {
+
if b.IsDefault {
+
defaultBranch = b.Name
+
}
+
}
+
+
base := defaultBranch
+
head := defaultBranch
+
+
params := r.URL.Query()
+
queryBase := params.Get("base")
+
queryHead := params.Get("head")
+
if queryBase != "" {
+
base = queryBase
+
}
+
if queryHead != "" {
+
head = queryHead
+
}
+
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
+
var tags types.RepoTagsResponse
+
if err := json.Unmarshal(tagBytes, &tags); err != nil {
+
l.Error("failed to decode XRPC tags response", "err", err)
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
+
return
+
}
+
+
repoinfo := f.RepoInfo(user)
+
+
rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{
+
LoggedInUser: user,
+
RepoInfo: repoinfo,
+
Branches: branches,
+
Tags: tags.Tags,
+
Base: base,
+
Head: head,
+
})
+
}
+
+
func (rp *Repo) Compare(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "RepoCompare")
+
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
+
var diffOpts types.DiffOpts
+
if d := r.URL.Query().Get("diff"); d == "split" {
+
diffOpts.Split = true
+
}
+
+
// if user is navigating to one of
+
// /compare/{base}/{head}
+
// /compare/{base}...{head}
+
base := chi.URLParam(r, "base")
+
head := chi.URLParam(r, "head")
+
if base == "" && head == "" {
+
rest := chi.URLParam(r, "*") // master...feature/xyz
+
parts := strings.SplitN(rest, "...", 2)
+
if len(parts) == 2 {
+
base = parts[0]
+
head = parts[1]
+
}
+
}
+
+
base, _ = url.PathUnescape(base)
+
head, _ = url.PathUnescape(head)
+
+
if base == "" || head == "" {
+
l.Error("invalid comparison")
+
rp.pages.Error404(w)
+
return
+
}
+
+
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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
+
var branches types.RepoBranchesResponse
+
if err := json.Unmarshal(branchBytes, &branches); err != nil {
+
l.Error("failed to decode XRPC branches response", "err", err)
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
+
return
+
}
+
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
+
var tags types.RepoTagsResponse
+
if err := json.Unmarshal(tagBytes, &tags); err != nil {
+
l.Error("failed to decode XRPC tags response", "err", 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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.compare", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
+
var formatPatch types.RepoFormatPatchResponse
+
if err := json.Unmarshal(compareBytes, &formatPatch); err != nil {
+
l.Error("failed to decode XRPC compare response", "err", err)
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
+
return
+
}
+
+
var diff types.NiceDiff
+
if formatPatch.CombinedPatchRaw != "" {
+
diff = patchutil.AsNiceDiff(formatPatch.CombinedPatchRaw, base)
+
} else {
+
diff = patchutil.AsNiceDiff(formatPatch.FormatPatchRaw, base)
+
}
+
+
repoinfo := f.RepoInfo(user)
+
+
rp.pages.RepoCompare(w, pages.RepoCompareParams{
+
LoggedInUser: user,
+
RepoInfo: repoinfo,
+
Branches: branches.Branches,
+
Tags: tags.Tags,
+
Base: base,
+
Head: head,
+
Diff: &diff,
+
DiffOpts: diffOpts,
+
})
+
+
}
+1 -1
appview/repo/feed.go
···
return fmt.Sprintf("%s in %s", base, repoName)
}
-
func (rp *Repo) RepoAtomFeed(w http.ResponseWriter, r *http.Request) {
+
func (rp *Repo) AtomFeed(w http.ResponseWriter, r *http.Request) {
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to fully resolve repo:", err)
+1 -1
appview/repo/index.go
···
"github.com/go-enry/go-enry/v2"
)
-
func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
+
func (rp *Repo) Index(w http.ResponseWriter, r *http.Request) {
l := rp.logger.With("handler", "RepoIndex")
ref := chi.URLParam(r, "ref")
+223
appview/repo/log.go
···
+
package repo
+
+
import (
+
"encoding/json"
+
"fmt"
+
"net/http"
+
"net/url"
+
"strconv"
+
+
"tangled.org/core/api/tangled"
+
"tangled.org/core/appview/commitverify"
+
"tangled.org/core/appview/db"
+
"tangled.org/core/appview/models"
+
"tangled.org/core/appview/pages"
+
xrpcclient "tangled.org/core/appview/xrpcclient"
+
"tangled.org/core/types"
+
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+
"github.com/go-chi/chi/v5"
+
"github.com/go-git/go-git/v5/plumbing"
+
)
+
+
func (rp *Repo) Log(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "RepoLog")
+
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to fully resolve repo", "err", err)
+
return
+
}
+
+
page := 1
+
if r.URL.Query().Get("page") != "" {
+
page, err = strconv.Atoi(r.URL.Query().Get("page"))
+
if err != nil {
+
page = 1
+
}
+
}
+
+
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
+
+
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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.log", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
+
var xrpcResp types.RepoLogResponse
+
if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil {
+
l.Error("failed to decode XRPC response", "err", err)
+
rp.pages.Error503(w)
+
return
+
}
+
+
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
+
tagMap := make(map[string][]string)
+
if tagBytes != nil {
+
var tagResp types.RepoTagsResponse
+
if err := json.Unmarshal(tagBytes, &tagResp); err == nil {
+
for _, tag := range tagResp.Tags {
+
hash := tag.Hash
+
if tag.Tag != nil {
+
hash = tag.Tag.Target.String()
+
}
+
tagMap[hash] = append(tagMap[hash], tag.Name)
+
}
+
}
+
}
+
+
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
+
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(xrpcResp.Commits), true)
+
if err != nil {
+
l.Error("failed to fetch email to did mapping", "err", err)
+
}
+
+
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits)
+
if err != nil {
+
l.Error("failed to GetVerifiedObjectCommits", "err", err)
+
}
+
+
repoInfo := f.RepoInfo(user)
+
+
var shas []string
+
for _, c := range xrpcResp.Commits {
+
shas = append(shas, c.Hash.String())
+
}
+
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
+
if err != nil {
+
l.Error("failed to getPipelineStatuses", "err", err)
+
// non-fatal
+
}
+
+
rp.pages.RepoLog(w, pages.RepoLogParams{
+
LoggedInUser: user,
+
TagMap: tagMap,
+
RepoInfo: repoInfo,
+
RepoLogResponse: xrpcResp,
+
EmailToDid: emailToDidMap,
+
VerifiedCommits: vc,
+
Pipelines: pipelines,
+
})
+
}
+
+
func (rp *Repo) Commit(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "RepoCommit")
+
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to fully resolve repo", "err", err)
+
return
+
}
+
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
+
+
var diffOpts types.DiffOpts
+
if d := r.URL.Query().Get("diff"); d == "split" {
+
diffOpts.Split = true
+
}
+
+
if !plumbing.IsHash(ref) {
+
rp.pages.Error404(w)
+
return
+
}
+
+
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.RepoDiff(r.Context(), xrpcc, ref, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.diff", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
+
var result types.RepoCommitResponse
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
+
l.Error("failed to decode XRPC response", "err", err)
+
rp.pages.Error503(w)
+
return
+
}
+
+
emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Committer.Email, result.Diff.Commit.Author.Email}, true)
+
if err != nil {
+
l.Error("failed to get email to did mapping", "err", err)
+
}
+
+
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff})
+
if err != nil {
+
l.Error("failed to GetVerifiedCommits", "err", err)
+
}
+
+
user := rp.oauth.GetUser(r)
+
repoInfo := f.RepoInfo(user)
+
pipelines, err := getPipelineStatuses(rp.db, repoInfo, []string{result.Diff.Commit.This})
+
if err != nil {
+
l.Error("failed to getPipelineStatuses", "err", err)
+
// non-fatal
+
}
+
var pipeline *models.Pipeline
+
if p, ok := pipelines[result.Diff.Commit.This]; ok {
+
pipeline = &p
+
}
+
+
rp.pages.RepoCommit(w, pages.RepoCommitParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
RepoCommitResponse: result,
+
EmailToDid: emailToDidMap,
+
VerifiedCommit: vc,
+
Pipeline: pipeline,
+
DiffOpts: diffOpts,
+
})
+
}
+1 -1
appview/repo/opengraph.go
···
return nil
}
-
func (rp *Repo) RepoOpenGraphSummary(w http.ResponseWriter, r *http.Request) {
+
func (rp *Repo) Opengraph(w http.ResponseWriter, r *http.Request) {
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
+71 -1439
appview/repo/repo.go
···
import (
"context"
"database/sql"
-
"encoding/json"
"errors"
"fmt"
-
"io"
"log/slog"
"net/http"
"net/url"
-
"path/filepath"
"slices"
-
"strconv"
"strings"
"time"
"tangled.org/core/api/tangled"
-
"tangled.org/core/appview/commitverify"
"tangled.org/core/appview/config"
"tangled.org/core/appview/db"
"tangled.org/core/appview/models"
"tangled.org/core/appview/notify"
"tangled.org/core/appview/oauth"
"tangled.org/core/appview/pages"
-
"tangled.org/core/appview/pages/markup"
"tangled.org/core/appview/reporesolver"
"tangled.org/core/appview/validator"
xrpcclient "tangled.org/core/appview/xrpcclient"
"tangled.org/core/eventconsumer"
"tangled.org/core/idresolver"
-
"tangled.org/core/patchutil"
"tangled.org/core/rbac"
"tangled.org/core/tid"
-
"tangled.org/core/types"
"tangled.org/core/xrpc/serviceauth"
comatproto "github.com/bluesky-social/indigo/api/atproto"
atpclient "github.com/bluesky-social/indigo/atproto/client"
"github.com/bluesky-social/indigo/atproto/syntax"
lexutil "github.com/bluesky-social/indigo/lex/util"
-
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
securejoin "github.com/cyphar/filepath-securejoin"
"github.com/go-chi/chi/v5"
-
"github.com/go-git/go-git/v5/plumbing"
)
type Repo struct {
···
}
}
-
func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "DownloadArchive")
-
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to get repo and knot", "err", err)
-
return
-
}
-
-
scheme := "http"
-
if !rp.config.Core.Dev {
-
scheme = "https"
-
}
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
Host: host,
-
}
-
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
-
archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo)
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.archive", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
// Set headers for file download, just pass along whatever the knot specifies
-
safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-")
-
filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename)
-
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
-
w.Header().Set("Content-Type", "application/gzip")
-
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes)))
-
-
// Write the archive data directly
-
w.Write(archiveBytes)
-
}
-
-
func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoLog")
-
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to fully resolve repo", "err", err)
-
return
-
}
-
-
page := 1
-
if r.URL.Query().Get("page") != "" {
-
page, err = strconv.Atoi(r.URL.Query().Get("page"))
-
if err != nil {
-
page = 1
-
}
-
}
-
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
-
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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.log", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
var xrpcResp types.RepoLogResponse
-
if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil {
-
l.Error("failed to decode XRPC response", "err", err)
-
rp.pages.Error503(w)
-
return
-
}
-
-
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
tagMap := make(map[string][]string)
-
if tagBytes != nil {
-
var tagResp types.RepoTagsResponse
-
if err := json.Unmarshal(tagBytes, &tagResp); err == nil {
-
for _, tag := range tagResp.Tags {
-
hash := tag.Hash
-
if tag.Tag != nil {
-
hash = tag.Tag.Target.String()
-
}
-
tagMap[hash] = append(tagMap[hash], tag.Name)
-
}
-
}
-
}
-
-
branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo)
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
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(xrpcResp.Commits), true)
-
if err != nil {
-
l.Error("failed to fetch email to did mapping", "err", err)
-
}
-
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits)
-
if err != nil {
-
l.Error("failed to GetVerifiedObjectCommits", "err", err)
-
}
-
-
repoInfo := f.RepoInfo(user)
-
-
var shas []string
-
for _, c := range xrpcResp.Commits {
-
shas = append(shas, c.Hash.String())
-
}
-
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
-
if err != nil {
-
l.Error("failed to getPipelineStatuses", "err", err)
-
// non-fatal
-
}
-
-
rp.pages.RepoLog(w, pages.RepoLogParams{
-
LoggedInUser: user,
-
TagMap: tagMap,
-
RepoInfo: repoInfo,
-
RepoLogResponse: xrpcResp,
-
EmailToDid: emailToDidMap,
-
VerifiedCommits: vc,
-
Pipelines: pipelines,
-
})
-
}
-
-
func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoCommit")
-
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to fully resolve repo", "err", err)
-
return
-
}
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
-
var diffOpts types.DiffOpts
-
if d := r.URL.Query().Get("diff"); d == "split" {
-
diffOpts.Split = true
-
}
-
-
if !plumbing.IsHash(ref) {
-
rp.pages.Error404(w)
-
return
-
}
-
-
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.RepoDiff(r.Context(), xrpcc, ref, repo)
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.diff", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
var result types.RepoCommitResponse
-
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
-
l.Error("failed to decode XRPC response", "err", err)
-
rp.pages.Error503(w)
-
return
-
}
-
-
emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Committer.Email, result.Diff.Commit.Author.Email}, true)
-
if err != nil {
-
l.Error("failed to get email to did mapping", "err", err)
-
}
-
-
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff})
-
if err != nil {
-
l.Error("failed to GetVerifiedCommits", "err", err)
-
}
-
-
user := rp.oauth.GetUser(r)
-
repoInfo := f.RepoInfo(user)
-
pipelines, err := getPipelineStatuses(rp.db, repoInfo, []string{result.Diff.Commit.This})
-
if err != nil {
-
l.Error("failed to getPipelineStatuses", "err", err)
-
// non-fatal
-
}
-
var pipeline *models.Pipeline
-
if p, ok := pipelines[result.Diff.Commit.This]; ok {
-
pipeline = &p
-
}
-
-
rp.pages.RepoCommit(w, pages.RepoCommitParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
RepoCommitResponse: result,
-
EmailToDid: emailToDidMap,
-
VerifiedCommit: vc,
-
Pipeline: pipeline,
-
DiffOpts: diffOpts,
-
})
-
}
-
-
func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoTree")
-
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to fully resolve repo", "err", err)
-
return
-
}
-
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
-
// if the tree path has a trailing slash, let's strip it
-
// so we don't 404
-
treePath := chi.URLParam(r, "*")
-
treePath, _ = url.PathUnescape(treePath)
-
treePath = strings.TrimSuffix(treePath, "/")
-
-
scheme := "http"
-
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)
-
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.tree", "err", xrpcerr)
-
rp.pages.Error503(w)
-
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
-
}
-
-
result := types.RepoTreeResponse{
-
Ref: xrpcResp.Ref,
-
Files: files,
-
}
-
-
if xrpcResp.Parent != nil {
-
result.Parent = *xrpcResp.Parent
-
}
-
if xrpcResp.Dotdot != nil {
-
result.DotDot = *xrpcResp.Dotdot
-
}
-
if xrpcResp.Readme != nil {
-
result.ReadmeFileName = xrpcResp.Readme.Filename
-
result.Readme = xrpcResp.Readme.Contents
-
}
-
-
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
-
// so we can safely redirect to the "parent" (which is the same file).
-
if len(result.Files) == 0 && result.Parent == treePath {
-
redirectTo := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), url.PathEscape(ref), result.Parent)
-
http.Redirect(w, r, redirectTo, http.StatusFound)
-
return
-
}
-
-
user := rp.oauth.GetUser(r)
-
-
var breadcrumbs [][]string
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
-
if treePath != "" {
-
for idx, elem := range strings.Split(treePath, "/") {
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
-
}
-
}
-
-
sortFiles(result.Files)
-
-
rp.pages.RepoTree(w, pages.RepoTreeParams{
-
LoggedInUser: user,
-
BreadCrumbs: breadcrumbs,
-
TreePath: treePath,
-
RepoInfo: f.RepoInfo(user),
-
RepoTreeResponse: result,
-
})
-
}
-
-
func (rp *Repo) RepoTags(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoTags")
-
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to get repo and knot", "err", err)
-
return
-
}
-
-
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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
var result types.RepoTagsResponse
-
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
-
l.Error("failed to decode XRPC response", "err", err)
-
rp.pages.Error503(w)
-
return
-
}
-
-
artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt()))
-
if err != nil {
-
l.Error("failed grab artifacts", "err", err)
-
return
-
}
-
-
// convert artifacts to map for easy UI building
-
artifactMap := make(map[plumbing.Hash][]models.Artifact)
-
for _, a := range artifacts {
-
artifactMap[a.Tag] = append(artifactMap[a.Tag], a)
-
}
-
-
var danglingArtifacts []models.Artifact
-
for _, a := range artifacts {
-
found := false
-
for _, t := range result.Tags {
-
if t.Tag != nil {
-
if t.Tag.Hash == a.Tag {
-
found = true
-
}
-
}
-
}
-
-
if !found {
-
danglingArtifacts = append(danglingArtifacts, a)
-
}
-
}
-
-
user := rp.oauth.GetUser(r)
-
rp.pages.RepoTags(w, pages.RepoTagsParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
RepoTagsResponse: result,
-
ArtifactMap: artifactMap,
-
DanglingArtifacts: danglingArtifacts,
-
})
-
}
-
-
func (rp *Repo) RepoBranches(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoBranches")
-
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to get repo and knot", "err", err)
-
return
-
}
-
-
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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
var result types.RepoBranchesResponse
-
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
-
l.Error("failed to decode XRPC response", "err", err)
-
rp.pages.Error503(w)
-
return
-
}
-
-
sortBranches(result.Branches)
-
-
user := rp.oauth.GetUser(r)
-
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
RepoBranchesResponse: result,
-
})
-
}
-
-
func (rp *Repo) DeleteBranch(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "DeleteBranch")
-
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to get repo and knot", "err", err)
-
return
-
}
-
-
noticeId := "delete-branch-error"
-
fail := func(msg string, err error) {
-
l.Error(msg, "err", err)
-
rp.pages.Notice(w, noticeId, msg)
-
}
-
-
branch := r.FormValue("branch")
-
if branch == "" {
-
fail("No branch provided.", nil)
-
return
-
}
-
-
client, err := rp.oauth.ServiceClient(
-
r,
-
oauth.WithService(f.Knot),
-
oauth.WithLxm(tangled.RepoDeleteBranchNSID),
-
oauth.WithDev(rp.config.Core.Dev),
-
)
-
if err != nil {
-
fail("Failed to connect to knotserver", nil)
-
return
-
}
-
-
err = tangled.RepoDeleteBranch(
-
r.Context(),
-
client,
-
&tangled.RepoDeleteBranch_Input{
-
Branch: branch,
-
Repo: f.RepoAt().String(),
-
},
-
)
-
if err := xrpcclient.HandleXrpcErr(err); err != nil {
-
fail(fmt.Sprintf("Failed to delete branch: %s", err), err)
-
return
-
}
-
l.Error("deleted branch from knot", "branch", branch, "repo", f.RepoAt())
-
-
rp.pages.HxRefresh(w)
-
}
-
-
func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoBlob")
-
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to get repo and knot", "err", err)
-
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 {
-
scheme = "https"
-
}
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
Host: host,
-
}
-
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
-
resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo)
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.blob", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
// Use XRPC response directly instead of converting to internal types
-
-
var breadcrumbs [][]string
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
-
if filePath != "" {
-
for idx, elem := range strings.Split(filePath, "/") {
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
-
}
-
}
-
-
showRendered := false
-
renderToggle := false
-
-
if markup.GetFormat(resp.Path) == markup.FormatMarkdown {
-
renderToggle = true
-
showRendered = r.URL.Query().Get("code") != "true"
-
}
-
-
var unsupported bool
-
var isImage bool
-
var isVideo bool
-
var contentSrc string
-
-
if resp.IsBinary != nil && *resp.IsBinary {
-
ext := strings.ToLower(filepath.Ext(resp.Path))
-
switch ext {
-
case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp":
-
isImage = true
-
case ".mp4", ".webm", ".ogg", ".mov", ".avi":
-
isVideo = true
-
default:
-
unsupported = true
-
}
-
-
// fetch the raw binary content using sh.tangled.repo.blob xrpc
-
repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
-
-
baseURL := &url.URL{
-
Scheme: scheme,
-
Host: f.Knot,
-
Path: "/xrpc/sh.tangled.repo.blob",
-
}
-
query := baseURL.Query()
-
query.Set("repo", repoName)
-
query.Set("ref", ref)
-
query.Set("path", filePath)
-
query.Set("raw", "true")
-
baseURL.RawQuery = query.Encode()
-
blobURL := baseURL.String()
-
-
contentSrc = blobURL
-
if !rp.config.Core.Dev {
-
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),
-
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,
-
})
-
}
-
-
func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoBlobRaw")
-
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to get repo and knot", "err", err)
-
w.WriteHeader(http.StatusBadRequest)
-
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 {
-
scheme = "https"
-
}
-
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
-
baseURL := &url.URL{
-
Scheme: scheme,
-
Host: f.Knot,
-
Path: "/xrpc/sh.tangled.repo.blob",
-
}
-
query := baseURL.Query()
-
query.Set("repo", repo)
-
query.Set("ref", ref)
-
query.Set("path", filePath)
-
query.Set("raw", "true")
-
baseURL.RawQuery = query.Encode()
-
blobURL := baseURL.String()
-
-
req, err := http.NewRequest("GET", blobURL, nil)
-
if err != nil {
-
l.Error("failed to create request", "err", err)
-
return
-
}
-
-
// forward the If-None-Match header
-
if clientETag := r.Header.Get("If-None-Match"); clientETag != "" {
-
req.Header.Set("If-None-Match", clientETag)
-
}
-
-
client := &http.Client{}
-
resp, err := client.Do(req)
-
if err != nil {
-
l.Error("failed to reach knotserver", "err", err)
-
rp.pages.Error503(w)
-
return
-
}
-
defer resp.Body.Close()
-
-
// forward 304 not modified
-
if resp.StatusCode == http.StatusNotModified {
-
w.WriteHeader(http.StatusNotModified)
-
return
-
}
-
-
if resp.StatusCode != http.StatusOK {
-
l.Error("knotserver returned non-OK status for raw blob", "url", blobURL, "statuscode", resp.StatusCode)
-
w.WriteHeader(resp.StatusCode)
-
_, _ = io.Copy(w, resp.Body)
-
return
-
}
-
-
contentType := resp.Header.Get("Content-Type")
-
body, err := io.ReadAll(resp.Body)
-
if err != nil {
-
l.Error("error reading response body from knotserver", "err", err)
-
w.WriteHeader(http.StatusInternalServerError)
-
return
-
}
-
-
if strings.HasPrefix(contentType, "text/") || isTextualMimeType(contentType) {
-
// 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
-
w.Header().Set("Content-Type", contentType)
-
w.Write(body)
-
} else {
-
w.WriteHeader(http.StatusUnsupportedMediaType)
-
w.Write([]byte("unsupported content type"))
-
return
-
}
-
}
-
// isTextualMimeType returns true if the MIME type represents textual content
-
// that should be served as text/plain
-
func isTextualMimeType(mimeType string) bool {
-
textualTypes := []string{
-
"application/json",
-
"application/xml",
-
"application/yaml",
-
"application/x-yaml",
-
"application/toml",
-
"application/javascript",
-
"application/ecmascript",
-
"message/",
-
}
-
-
return slices.Contains(textualTypes, mimeType)
-
}
// modify the spindle configured for this repo
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
···
Rkey: rkey,
SubjectDid: collaboratorIdent.DID,
RepoAt: f.RepoAt(),
-
Created: createdAt,
-
})
-
if err != nil {
-
fail("Failed to add collaborator.", err)
-
return
-
}
-
-
err = tx.Commit()
-
if err != nil {
-
fail("Failed to add collaborator.", err)
-
return
-
}
-
-
err = rp.enforcer.E.SavePolicy()
-
if err != nil {
-
fail("Failed to update collaborator permissions.", err)
-
return
-
}
-
-
// clear aturi to when everything is successful
-
aturi = ""
-
-
rp.pages.HxRefresh(w)
-
}
-
-
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
-
user := rp.oauth.GetUser(r)
-
l := rp.logger.With("handler", "DeleteRepo")
-
-
noticeId := "operation-error"
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to get repo and knot", "err", err)
-
return
-
}
-
-
// remove record from pds
-
atpClient, err := rp.oauth.AuthorizedClient(r)
-
if err != nil {
-
l.Error("failed to get authorized client", "err", err)
-
return
-
}
-
_, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{
-
Collection: tangled.RepoNSID,
-
Repo: user.Did,
-
Rkey: f.Rkey,
-
})
-
if err != nil {
-
l.Error("failed to delete record", "err", err)
-
rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.")
-
return
-
}
-
l.Info("removed repo record", "aturi", f.RepoAt().String())
-
-
client, err := rp.oauth.ServiceClient(
-
r,
-
oauth.WithService(f.Knot),
-
oauth.WithLxm(tangled.RepoDeleteNSID),
-
oauth.WithDev(rp.config.Core.Dev),
-
)
-
if err != nil {
-
l.Error("failed to connect to knot server", "err", err)
-
return
-
}
-
-
err = tangled.RepoDelete(
-
r.Context(),
-
client,
-
&tangled.RepoDelete_Input{
-
Did: f.OwnerDid(),
-
Name: f.Name,
-
Rkey: f.Rkey,
-
},
-
)
-
if err := xrpcclient.HandleXrpcErr(err); err != nil {
-
rp.pages.Notice(w, noticeId, err.Error())
-
return
-
}
-
l.Info("deleted repo from knot")
-
-
tx, err := rp.db.BeginTx(r.Context(), nil)
-
if err != nil {
-
l.Error("failed to start tx")
-
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
-
return
-
}
-
defer func() {
-
tx.Rollback()
-
err = rp.enforcer.E.LoadPolicy()
-
if err != nil {
-
l.Error("failed to rollback policies")
-
}
-
}()
-
-
// remove collaborator RBAC
-
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
-
if err != nil {
-
rp.pages.Notice(w, noticeId, "Failed to remove collaborators")
-
return
-
}
-
for _, c := range repoCollaborators {
-
did := c[0]
-
rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
-
}
-
l.Info("removed collaborators")
-
-
// remove repo RBAC
-
err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
-
if err != nil {
-
rp.pages.Notice(w, noticeId, "Failed to update RBAC rules")
-
return
-
}
-
-
// remove repo from db
-
err = db.RemoveRepo(tx, f.OwnerDid(), f.Name)
-
if err != nil {
-
rp.pages.Notice(w, noticeId, "Failed to update appview")
-
return
-
}
-
l.Info("removed repo from db")
-
-
err = tx.Commit()
-
if err != nil {
-
l.Error("failed to commit changes", "err", err)
-
http.Error(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
err = rp.enforcer.E.SavePolicy()
-
if err != nil {
-
l.Error("failed to update ACLs", "err", err)
-
http.Error(w, err.Error(), http.StatusInternalServerError)
-
return
-
}
-
-
rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid()))
-
}
-
-
func (rp *Repo) EditBaseSettings(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "EditBaseSettings")
-
-
noticeId := "repo-base-settings-error"
-
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to get repo and knot", "err", err)
-
w.WriteHeader(http.StatusBadRequest)
-
return
-
}
-
-
client, err := rp.oauth.AuthorizedClient(r)
-
if err != nil {
-
l.Error("failed to get client")
-
rp.pages.Notice(w, noticeId, "Failed to update repository information, try again later.")
-
return
-
}
-
-
var (
-
description = r.FormValue("description")
-
website = r.FormValue("website")
-
topicStr = r.FormValue("topics")
-
)
-
-
err = rp.validator.ValidateURI(website)
-
if err != nil {
-
l.Error("invalid uri", "err", err)
-
rp.pages.Notice(w, noticeId, err.Error())
-
return
-
}
-
-
topics, err := rp.validator.ValidateRepoTopicStr(topicStr)
-
if err != nil {
-
l.Error("invalid topics", "err", err)
-
rp.pages.Notice(w, noticeId, err.Error())
-
return
-
}
-
l.Debug("got", "topicsStr", topicStr, "topics", topics)
-
-
newRepo := f.Repo
-
newRepo.Description = description
-
newRepo.Website = website
-
newRepo.Topics = topics
-
record := newRepo.AsRecord()
-
-
tx, err := rp.db.BeginTx(r.Context(), nil)
-
if err != nil {
-
l.Error("failed to begin transaction", "err", err)
-
rp.pages.Notice(w, noticeId, "Failed to save repository information.")
-
return
-
}
-
defer tx.Rollback()
-
-
err = db.PutRepo(tx, newRepo)
-
if err != nil {
-
l.Error("failed to update repository", "err", err)
-
rp.pages.Notice(w, noticeId, "Failed to save repository information.")
-
return
-
}
-
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey)
-
if err != nil {
-
// failed to get record
-
l.Error("failed to get repo record", "err", err)
-
rp.pages.Notice(w, noticeId, "Failed to save repository information, no record found on PDS.")
-
return
-
}
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
-
Collection: tangled.RepoNSID,
-
Repo: newRepo.Did,
-
Rkey: newRepo.Rkey,
-
SwapRecord: ex.Cid,
-
Record: &lexutil.LexiconTypeDecoder{
-
Val: &record,
-
},
+
Created: createdAt,
})
-
if err != nil {
-
l.Error("failed to perferom update-repo query", "err", err)
-
// failed to get record
-
rp.pages.Notice(w, noticeId, "Failed to save repository information, unable to save to PDS.")
+
fail("Failed to add collaborator.", err)
return
}
err = tx.Commit()
if err != nil {
-
l.Error("failed to commit", "err", err)
+
fail("Failed to add collaborator.", err)
+
return
+
}
+
+
err = rp.enforcer.E.SavePolicy()
+
if err != nil {
+
fail("Failed to update collaborator permissions.", err)
+
return
}
+
// clear aturi to when everything is successful
+
aturi = ""
+
rp.pages.HxRefresh(w)
}
-
func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "SetDefaultBranch")
+
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
l := rp.logger.With("handler", "DeleteRepo")
+
noticeId := "operation-error"
f, err := rp.repoResolver.Resolve(r)
if err != nil {
l.Error("failed to get repo and knot", "err", err)
return
}
-
noticeId := "operation-error"
-
branch := r.FormValue("branch")
-
if branch == "" {
-
http.Error(w, "malformed form", http.StatusBadRequest)
+
// remove record from pds
+
atpClient, err := rp.oauth.AuthorizedClient(r)
+
if err != nil {
+
l.Error("failed to get authorized client", "err", err)
+
return
+
}
+
_, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{
+
Collection: tangled.RepoNSID,
+
Repo: user.Did,
+
Rkey: f.Rkey,
+
})
+
if err != nil {
+
l.Error("failed to delete record", "err", err)
+
rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.")
return
}
+
l.Info("removed repo record", "aturi", f.RepoAt().String())
client, err := rp.oauth.ServiceClient(
r,
oauth.WithService(f.Knot),
-
oauth.WithLxm(tangled.RepoSetDefaultBranchNSID),
+
oauth.WithLxm(tangled.RepoDeleteNSID),
oauth.WithDev(rp.config.Core.Dev),
)
if err != nil {
l.Error("failed to connect to knot server", "err", err)
-
rp.pages.Notice(w, noticeId, "Failed to connect to knot server.")
return
}
-
xe := tangled.RepoSetDefaultBranch(
+
err = tangled.RepoDelete(
r.Context(),
client,
-
&tangled.RepoSetDefaultBranch_Input{
-
Repo: f.RepoAt().String(),
-
DefaultBranch: branch,
+
&tangled.RepoDelete_Input{
+
Did: f.OwnerDid(),
+
Name: f.Name,
+
Rkey: f.Rkey,
},
)
-
if err := xrpcclient.HandleXrpcErr(xe); err != nil {
-
l.Error("xrpc failed", "err", xe)
+
if err := xrpcclient.HandleXrpcErr(err); err != nil {
rp.pages.Notice(w, noticeId, err.Error())
return
}
+
l.Info("deleted repo from knot")
-
rp.pages.HxRefresh(w)
-
}
-
-
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
-
user := rp.oauth.GetUser(r)
-
l := rp.logger.With("handler", "Secrets")
-
l = l.With("did", user.Did)
-
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to get repo and knot", "err", err)
-
return
-
}
-
-
if f.Spindle == "" {
-
l.Error("empty spindle cannot add/rm secret", "err", err)
-
return
-
}
-
-
lxm := tangled.RepoAddSecretNSID
-
if r.Method == http.MethodDelete {
-
lxm = tangled.RepoRemoveSecretNSID
-
}
-
-
spindleClient, err := rp.oauth.ServiceClient(
-
r,
-
oauth.WithService(f.Spindle),
-
oauth.WithLxm(lxm),
-
oauth.WithExp(60),
-
oauth.WithDev(rp.config.Core.Dev),
-
)
+
tx, err := rp.db.BeginTx(r.Context(), nil)
if err != nil {
-
l.Error("failed to create spindle client", "err", err)
-
return
-
}
-
-
key := r.FormValue("key")
-
if key == "" {
-
w.WriteHeader(http.StatusBadRequest)
+
l.Error("failed to start tx")
+
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
return
}
-
-
switch r.Method {
-
case http.MethodPut:
-
errorId := "add-secret-error"
-
-
value := r.FormValue("value")
-
if value == "" {
-
w.WriteHeader(http.StatusBadRequest)
-
return
-
}
-
-
err = tangled.RepoAddSecret(
-
r.Context(),
-
spindleClient,
-
&tangled.RepoAddSecret_Input{
-
Repo: f.RepoAt().String(),
-
Key: key,
-
Value: value,
-
},
-
)
-
if err != nil {
-
l.Error("Failed to add secret.", "err", err)
-
rp.pages.Notice(w, errorId, "Failed to add secret.")
-
return
-
}
-
-
case http.MethodDelete:
-
errorId := "operation-error"
-
-
err = tangled.RepoRemoveSecret(
-
r.Context(),
-
spindleClient,
-
&tangled.RepoRemoveSecret_Input{
-
Repo: f.RepoAt().String(),
-
Key: key,
-
},
-
)
+
defer func() {
+
tx.Rollback()
+
err = rp.enforcer.E.LoadPolicy()
if err != nil {
-
l.Error("Failed to delete secret.", "err", err)
-
rp.pages.Notice(w, errorId, "Failed to delete secret.")
-
return
+
l.Error("failed to rollback policies")
}
-
}
-
-
rp.pages.HxRefresh(w)
-
}
-
-
type tab = map[string]any
-
-
var (
-
// would be great to have ordered maps right about now
-
settingsTabs []tab = []tab{
-
{"Name": "general", "Icon": "sliders-horizontal"},
-
{"Name": "access", "Icon": "users"},
-
{"Name": "pipelines", "Icon": "layers-2"},
-
}
-
)
-
-
func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
-
tabVal := r.URL.Query().Get("tab")
-
if tabVal == "" {
-
tabVal = "general"
-
}
-
-
switch tabVal {
-
case "general":
-
rp.generalSettings(w, r)
-
-
case "access":
-
rp.accessSettings(w, r)
-
-
case "pipelines":
-
rp.pipelineSettings(w, r)
-
}
-
}
-
-
func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "generalSettings")
-
-
f, err := rp.repoResolver.Resolve(r)
-
user := rp.oauth.GetUser(r)
-
-
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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
-
rp.pages.Error503(w)
+
// remove collaborator RBAC
+
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
+
if err != nil {
+
rp.pages.Notice(w, noticeId, "Failed to remove collaborators")
return
}
-
-
var result types.RepoBranchesResponse
-
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
-
l.Error("failed to decode XRPC response", "err", err)
-
rp.pages.Error503(w)
-
return
+
for _, c := range repoCollaborators {
+
did := c[0]
+
rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
}
+
l.Info("removed collaborators")
-
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", models.DefaultLabelDefs()))
+
// remove repo RBAC
+
err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
if err != nil {
-
l.Error("failed to fetch labels", "err", err)
-
rp.pages.Error503(w)
+
rp.pages.Notice(w, noticeId, "Failed to update RBAC rules")
return
}
-
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
+
// remove repo from db
+
err = db.RemoveRepo(tx, f.OwnerDid(), f.Name)
if err != nil {
-
l.Error("failed to fetch labels", "err", err)
-
rp.pages.Error503(w)
+
rp.pages.Notice(w, noticeId, "Failed to update appview")
return
}
-
// remove default labels from the labels list, if present
-
defaultLabelMap := make(map[string]bool)
-
for _, dl := range defaultLabels {
-
defaultLabelMap[dl.AtUri().String()] = true
-
}
-
n := 0
-
for _, l := range labels {
-
if !defaultLabelMap[l.AtUri().String()] {
-
labels[n] = l
-
n++
-
}
-
}
-
labels = labels[:n]
-
-
subscribedLabels := make(map[string]struct{})
-
for _, l := range f.Repo.Labels {
-
subscribedLabels[l] = struct{}{}
-
}
-
-
// if there is atleast 1 unsubbed default label, show the "subscribe all" button,
-
// if all default labels are subbed, show the "unsubscribe all" button
-
shouldSubscribeAll := false
-
for _, dl := range defaultLabels {
-
if _, ok := subscribedLabels[dl.AtUri().String()]; !ok {
-
// one of the default labels is not subscribed to
-
shouldSubscribeAll = true
-
break
-
}
-
}
-
-
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
Branches: result.Branches,
-
Labels: labels,
-
DefaultLabels: defaultLabels,
-
SubscribedLabels: subscribedLabels,
-
ShouldSubscribeAll: shouldSubscribeAll,
-
Tabs: settingsTabs,
-
Tab: "general",
-
})
-
}
-
-
func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "accessSettings")
-
-
f, err := rp.repoResolver.Resolve(r)
-
user := rp.oauth.GetUser(r)
+
l.Info("removed repo from db")
-
repoCollaborators, err := f.Collaborators(r.Context())
+
err = tx.Commit()
if err != nil {
-
l.Error("failed to get collaborators", "err", err)
+
l.Error("failed to commit changes", "err", err)
+
http.Error(w, err.Error(), http.StatusInternalServerError)
+
return
}
-
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
Tabs: settingsTabs,
-
Tab: "access",
-
Collaborators: repoCollaborators,
-
})
-
}
-
-
func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "pipelineSettings")
-
-
f, err := rp.repoResolver.Resolve(r)
-
user := rp.oauth.GetUser(r)
-
-
// all spindles that the repo owner is a member of
-
spindles, err := rp.enforcer.GetSpindlesForUser(f.OwnerDid())
+
err = rp.enforcer.E.SavePolicy()
if err != nil {
-
l.Error("failed to fetch spindles", "err", err)
+
l.Error("failed to update ACLs", "err", err)
+
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
-
var secrets []*tangled.RepoListSecrets_Secret
-
if f.Spindle != "" {
-
if spindleClient, err := rp.oauth.ServiceClient(
-
r,
-
oauth.WithService(f.Spindle),
-
oauth.WithLxm(tangled.RepoListSecretsNSID),
-
oauth.WithExp(60),
-
oauth.WithDev(rp.config.Core.Dev),
-
); err != nil {
-
l.Error("failed to create spindle client", "err", err)
-
} else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil {
-
l.Error("failed to fetch secrets", "err", err)
-
} else {
-
secrets = resp.Secrets
-
}
-
}
-
-
slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int {
-
return strings.Compare(a.Key, b.Key)
-
})
-
-
var dids []string
-
for _, s := range secrets {
-
dids = append(dids, s.CreatedBy)
-
}
-
resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids)
-
-
// convert to a more manageable form
-
var niceSecret []map[string]any
-
for id, s := range secrets {
-
when, _ := time.Parse(time.RFC3339, s.CreatedAt)
-
niceSecret = append(niceSecret, map[string]any{
-
"Id": id,
-
"Key": s.Key,
-
"CreatedAt": when,
-
"CreatedBy": resolvedIdents[id].Handle.String(),
-
})
-
}
-
-
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(user),
-
Tabs: settingsTabs,
-
Tab: "pipelines",
-
Spindles: spindles,
-
CurrentSpindle: f.Spindle,
-
Secrets: niceSecret,
-
})
+
rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid()))
}
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
···
})
return err
-
-
func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoCompareNew")
-
-
user := rp.oauth.GetUser(r)
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to get repo and knot", "err", err)
-
return
-
}
-
-
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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
var branchResult types.RepoBranchesResponse
-
if err := json.Unmarshal(branchBytes, &branchResult); err != nil {
-
l.Error("failed to decode XRPC branches response", "err", err)
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
-
return
-
}
-
branches := branchResult.Branches
-
-
sortBranches(branches)
-
-
var defaultBranch string
-
for _, b := range branches {
-
if b.IsDefault {
-
defaultBranch = b.Name
-
}
-
}
-
-
base := defaultBranch
-
head := defaultBranch
-
-
params := r.URL.Query()
-
queryBase := params.Get("base")
-
queryHead := params.Get("head")
-
if queryBase != "" {
-
base = queryBase
-
}
-
if queryHead != "" {
-
head = queryHead
-
}
-
-
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
var tags types.RepoTagsResponse
-
if err := json.Unmarshal(tagBytes, &tags); err != nil {
-
l.Error("failed to decode XRPC tags response", "err", err)
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
-
return
-
}
-
-
repoinfo := f.RepoInfo(user)
-
-
rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{
-
LoggedInUser: user,
-
RepoInfo: repoinfo,
-
Branches: branches,
-
Tags: tags.Tags,
-
Base: base,
-
Head: head,
-
})
-
}
-
-
func (rp *Repo) RepoCompare(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoCompare")
-
-
user := rp.oauth.GetUser(r)
-
f, err := rp.repoResolver.Resolve(r)
-
if err != nil {
-
l.Error("failed to get repo and knot", "err", err)
-
return
-
}
-
-
var diffOpts types.DiffOpts
-
if d := r.URL.Query().Get("diff"); d == "split" {
-
diffOpts.Split = true
-
}
-
-
// if user is navigating to one of
-
// /compare/{base}/{head}
-
// /compare/{base}...{head}
-
base := chi.URLParam(r, "base")
-
head := chi.URLParam(r, "head")
-
if base == "" && head == "" {
-
rest := chi.URLParam(r, "*") // master...feature/xyz
-
parts := strings.SplitN(rest, "...", 2)
-
if len(parts) == 2 {
-
base = parts[0]
-
head = parts[1]
-
}
-
}
-
-
base, _ = url.PathUnescape(base)
-
head, _ = url.PathUnescape(head)
-
-
if base == "" || head == "" {
-
l.Error("invalid comparison")
-
rp.pages.Error404(w)
-
return
-
}
-
-
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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
var branches types.RepoBranchesResponse
-
if err := json.Unmarshal(branchBytes, &branches); err != nil {
-
l.Error("failed to decode XRPC branches response", "err", err)
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
-
return
-
}
-
-
tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo)
-
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
var tags types.RepoTagsResponse
-
if err := json.Unmarshal(tagBytes, &tags); err != nil {
-
l.Error("failed to decode XRPC tags response", "err", 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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
-
l.Error("failed to call XRPC repo.compare", "err", xrpcerr)
-
rp.pages.Error503(w)
-
return
-
}
-
-
var formatPatch types.RepoFormatPatchResponse
-
if err := json.Unmarshal(compareBytes, &formatPatch); err != nil {
-
l.Error("failed to decode XRPC compare response", "err", err)
-
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
-
return
-
}
-
-
var diff types.NiceDiff
-
if formatPatch.CombinedPatchRaw != "" {
-
diff = patchutil.AsNiceDiff(formatPatch.CombinedPatchRaw, base)
-
} else {
-
diff = patchutil.AsNiceDiff(formatPatch.FormatPatchRaw, base)
-
}
-
-
repoinfo := f.RepoInfo(user)
-
-
rp.pages.RepoCompare(w, pages.RepoCompareParams{
-
LoggedInUser: user,
-
RepoInfo: repoinfo,
-
Branches: branches.Branches,
-
Tags: tags.Tags,
-
Base: base,
-
Head: head,
-
Diff: &diff,
-
DiffOpts: diffOpts,
-
})
-
-
}
+14 -14
appview/repo/router.go
···
func (rp *Repo) Router(mw *middleware.Middleware) http.Handler {
r := chi.NewRouter()
-
r.Get("/", rp.RepoIndex)
-
r.Get("/opengraph", rp.RepoOpenGraphSummary)
-
r.Get("/feed.atom", rp.RepoAtomFeed)
-
r.Get("/commits/{ref}", rp.RepoLog)
+
r.Get("/", rp.Index)
+
r.Get("/opengraph", rp.Opengraph)
+
r.Get("/feed.atom", rp.AtomFeed)
+
r.Get("/commits/{ref}", rp.Log)
r.Route("/tree/{ref}", func(r chi.Router) {
-
r.Get("/", rp.RepoIndex)
-
r.Get("/*", rp.RepoTree)
+
r.Get("/", rp.Index)
+
r.Get("/*", rp.Tree)
})
-
r.Get("/commit/{ref}", rp.RepoCommit)
-
r.Get("/branches", rp.RepoBranches)
+
r.Get("/commit/{ref}", rp.Commit)
+
r.Get("/branches", rp.Branches)
r.Delete("/branches", rp.DeleteBranch)
r.Route("/tags", func(r chi.Router) {
-
r.Get("/", rp.RepoTags)
+
r.Get("/", rp.Tags)
r.Route("/{tag}", func(r chi.Router) {
r.Get("/download/{file}", rp.DownloadArtifact)
···
})
})
})
-
r.Get("/blob/{ref}/*", rp.RepoBlob)
+
r.Get("/blob/{ref}/*", rp.Blob)
r.Get("/raw/{ref}/*", rp.RepoBlobRaw)
// intentionally doesn't use /* as this isn't
···
})
r.Route("/compare", func(r chi.Router) {
-
r.Get("/", rp.RepoCompareNew) // start an new comparison
+
r.Get("/", rp.CompareNew) // start an new comparison
// we have to wildcard here since we want to support GitHub's compare syntax
// /compare/{ref1}...{ref2}
// for example:
// /compare/master...some/feature
// /compare/master...example.com:another/feature <- this is a fork
-
r.Get("/{base}/{head}", rp.RepoCompare)
-
r.Get("/*", rp.RepoCompare)
+
r.Get("/{base}/{head}", rp.Compare)
+
r.Get("/*", rp.Compare)
})
// label panel in issues/pulls/discussions/tasks
···
r.Group(func(r chi.Router) {
r.Use(middleware.AuthMiddleware(rp.oauth))
r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) {
-
r.Get("/", rp.RepoSettings)
+
r.Get("/", rp.Settings)
r.With(mw.RepoPermissionMiddleware("repo:owner")).Put("/base", rp.EditBaseSettings)
r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/spindle", rp.EditSpindle)
r.With(mw.RepoPermissionMiddleware("repo:owner")).Put("/label", rp.AddLabelDef)
+443
appview/repo/settings.go
···
+
package repo
+
+
import (
+
"encoding/json"
+
"fmt"
+
"net/http"
+
"slices"
+
"strings"
+
"time"
+
+
"tangled.org/core/api/tangled"
+
"tangled.org/core/appview/db"
+
"tangled.org/core/appview/models"
+
"tangled.org/core/appview/oauth"
+
"tangled.org/core/appview/pages"
+
xrpcclient "tangled.org/core/appview/xrpcclient"
+
"tangled.org/core/types"
+
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
+
lexutil "github.com/bluesky-social/indigo/lex/util"
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+
)
+
+
type tab = map[string]any
+
+
var (
+
// would be great to have ordered maps right about now
+
settingsTabs []tab = []tab{
+
{"Name": "general", "Icon": "sliders-horizontal"},
+
{"Name": "access", "Icon": "users"},
+
{"Name": "pipelines", "Icon": "layers-2"},
+
}
+
)
+
+
func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "SetDefaultBranch")
+
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
+
noticeId := "operation-error"
+
branch := r.FormValue("branch")
+
if branch == "" {
+
http.Error(w, "malformed form", http.StatusBadRequest)
+
return
+
}
+
+
client, err := rp.oauth.ServiceClient(
+
r,
+
oauth.WithService(f.Knot),
+
oauth.WithLxm(tangled.RepoSetDefaultBranchNSID),
+
oauth.WithDev(rp.config.Core.Dev),
+
)
+
if err != nil {
+
l.Error("failed to connect to knot server", "err", err)
+
rp.pages.Notice(w, noticeId, "Failed to connect to knot server.")
+
return
+
}
+
+
xe := tangled.RepoSetDefaultBranch(
+
r.Context(),
+
client,
+
&tangled.RepoSetDefaultBranch_Input{
+
Repo: f.RepoAt().String(),
+
DefaultBranch: branch,
+
},
+
)
+
if err := xrpcclient.HandleXrpcErr(xe); err != nil {
+
l.Error("xrpc failed", "err", xe)
+
rp.pages.Notice(w, noticeId, err.Error())
+
return
+
}
+
+
rp.pages.HxRefresh(w)
+
}
+
+
func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
l := rp.logger.With("handler", "Secrets")
+
l = l.With("did", user.Did)
+
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
+
if f.Spindle == "" {
+
l.Error("empty spindle cannot add/rm secret", "err", err)
+
return
+
}
+
+
lxm := tangled.RepoAddSecretNSID
+
if r.Method == http.MethodDelete {
+
lxm = tangled.RepoRemoveSecretNSID
+
}
+
+
spindleClient, err := rp.oauth.ServiceClient(
+
r,
+
oauth.WithService(f.Spindle),
+
oauth.WithLxm(lxm),
+
oauth.WithExp(60),
+
oauth.WithDev(rp.config.Core.Dev),
+
)
+
if err != nil {
+
l.Error("failed to create spindle client", "err", err)
+
return
+
}
+
+
key := r.FormValue("key")
+
if key == "" {
+
w.WriteHeader(http.StatusBadRequest)
+
return
+
}
+
+
switch r.Method {
+
case http.MethodPut:
+
errorId := "add-secret-error"
+
+
value := r.FormValue("value")
+
if value == "" {
+
w.WriteHeader(http.StatusBadRequest)
+
return
+
}
+
+
err = tangled.RepoAddSecret(
+
r.Context(),
+
spindleClient,
+
&tangled.RepoAddSecret_Input{
+
Repo: f.RepoAt().String(),
+
Key: key,
+
Value: value,
+
},
+
)
+
if err != nil {
+
l.Error("Failed to add secret.", "err", err)
+
rp.pages.Notice(w, errorId, "Failed to add secret.")
+
return
+
}
+
+
case http.MethodDelete:
+
errorId := "operation-error"
+
+
err = tangled.RepoRemoveSecret(
+
r.Context(),
+
spindleClient,
+
&tangled.RepoRemoveSecret_Input{
+
Repo: f.RepoAt().String(),
+
Key: key,
+
},
+
)
+
if err != nil {
+
l.Error("Failed to delete secret.", "err", err)
+
rp.pages.Notice(w, errorId, "Failed to delete secret.")
+
return
+
}
+
}
+
+
rp.pages.HxRefresh(w)
+
}
+
+
func (rp *Repo) Settings(w http.ResponseWriter, r *http.Request) {
+
tabVal := r.URL.Query().Get("tab")
+
if tabVal == "" {
+
tabVal = "general"
+
}
+
+
switch tabVal {
+
case "general":
+
rp.generalSettings(w, r)
+
+
case "access":
+
rp.accessSettings(w, r)
+
+
case "pipelines":
+
rp.pipelineSettings(w, r)
+
}
+
}
+
+
func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "generalSettings")
+
+
f, err := rp.repoResolver.Resolve(r)
+
user := rp.oauth.GetUser(r)
+
+
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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.branches", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
+
var result types.RepoBranchesResponse
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
+
l.Error("failed to decode XRPC response", "err", err)
+
rp.pages.Error503(w)
+
return
+
}
+
+
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", models.DefaultLabelDefs()))
+
if err != nil {
+
l.Error("failed to fetch labels", "err", err)
+
rp.pages.Error503(w)
+
return
+
}
+
+
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
+
if err != nil {
+
l.Error("failed to fetch labels", "err", err)
+
rp.pages.Error503(w)
+
return
+
}
+
// remove default labels from the labels list, if present
+
defaultLabelMap := make(map[string]bool)
+
for _, dl := range defaultLabels {
+
defaultLabelMap[dl.AtUri().String()] = true
+
}
+
n := 0
+
for _, l := range labels {
+
if !defaultLabelMap[l.AtUri().String()] {
+
labels[n] = l
+
n++
+
}
+
}
+
labels = labels[:n]
+
+
subscribedLabels := make(map[string]struct{})
+
for _, l := range f.Repo.Labels {
+
subscribedLabels[l] = struct{}{}
+
}
+
+
// if there is atleast 1 unsubbed default label, show the "subscribe all" button,
+
// if all default labels are subbed, show the "unsubscribe all" button
+
shouldSubscribeAll := false
+
for _, dl := range defaultLabels {
+
if _, ok := subscribedLabels[dl.AtUri().String()]; !ok {
+
// one of the default labels is not subscribed to
+
shouldSubscribeAll = true
+
break
+
}
+
}
+
+
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
Branches: result.Branches,
+
Labels: labels,
+
DefaultLabels: defaultLabels,
+
SubscribedLabels: subscribedLabels,
+
ShouldSubscribeAll: shouldSubscribeAll,
+
Tabs: settingsTabs,
+
Tab: "general",
+
})
+
}
+
+
func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "accessSettings")
+
+
f, err := rp.repoResolver.Resolve(r)
+
user := rp.oauth.GetUser(r)
+
+
repoCollaborators, err := f.Collaborators(r.Context())
+
if err != nil {
+
l.Error("failed to get collaborators", "err", err)
+
}
+
+
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
Tabs: settingsTabs,
+
Tab: "access",
+
Collaborators: repoCollaborators,
+
})
+
}
+
+
func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "pipelineSettings")
+
+
f, err := rp.repoResolver.Resolve(r)
+
user := rp.oauth.GetUser(r)
+
+
// all spindles that the repo owner is a member of
+
spindles, err := rp.enforcer.GetSpindlesForUser(f.OwnerDid())
+
if err != nil {
+
l.Error("failed to fetch spindles", "err", err)
+
return
+
}
+
+
var secrets []*tangled.RepoListSecrets_Secret
+
if f.Spindle != "" {
+
if spindleClient, err := rp.oauth.ServiceClient(
+
r,
+
oauth.WithService(f.Spindle),
+
oauth.WithLxm(tangled.RepoListSecretsNSID),
+
oauth.WithExp(60),
+
oauth.WithDev(rp.config.Core.Dev),
+
); err != nil {
+
l.Error("failed to create spindle client", "err", err)
+
} else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil {
+
l.Error("failed to fetch secrets", "err", err)
+
} else {
+
secrets = resp.Secrets
+
}
+
}
+
+
slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int {
+
return strings.Compare(a.Key, b.Key)
+
})
+
+
var dids []string
+
for _, s := range secrets {
+
dids = append(dids, s.CreatedBy)
+
}
+
resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids)
+
+
// convert to a more manageable form
+
var niceSecret []map[string]any
+
for id, s := range secrets {
+
when, _ := time.Parse(time.RFC3339, s.CreatedAt)
+
niceSecret = append(niceSecret, map[string]any{
+
"Id": id,
+
"Key": s.Key,
+
"CreatedAt": when,
+
"CreatedBy": resolvedIdents[id].Handle.String(),
+
})
+
}
+
+
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
Tabs: settingsTabs,
+
Tab: "pipelines",
+
Spindles: spindles,
+
CurrentSpindle: f.Spindle,
+
Secrets: niceSecret,
+
})
+
}
+
+
func (rp *Repo) EditBaseSettings(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "EditBaseSettings")
+
+
noticeId := "repo-base-settings-error"
+
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
w.WriteHeader(http.StatusBadRequest)
+
return
+
}
+
+
client, err := rp.oauth.AuthorizedClient(r)
+
if err != nil {
+
l.Error("failed to get client")
+
rp.pages.Notice(w, noticeId, "Failed to update repository information, try again later.")
+
return
+
}
+
+
var (
+
description = r.FormValue("description")
+
website = r.FormValue("website")
+
topicStr = r.FormValue("topics")
+
)
+
+
err = rp.validator.ValidateURI(website)
+
if err != nil {
+
l.Error("invalid uri", "err", err)
+
rp.pages.Notice(w, noticeId, err.Error())
+
return
+
}
+
+
topics, err := rp.validator.ValidateRepoTopicStr(topicStr)
+
if err != nil {
+
l.Error("invalid topics", "err", err)
+
rp.pages.Notice(w, noticeId, err.Error())
+
return
+
}
+
l.Debug("got", "topicsStr", topicStr, "topics", topics)
+
+
newRepo := f.Repo
+
newRepo.Description = description
+
newRepo.Website = website
+
newRepo.Topics = topics
+
record := newRepo.AsRecord()
+
+
tx, err := rp.db.BeginTx(r.Context(), nil)
+
if err != nil {
+
l.Error("failed to begin transaction", "err", err)
+
rp.pages.Notice(w, noticeId, "Failed to save repository information.")
+
return
+
}
+
defer tx.Rollback()
+
+
err = db.PutRepo(tx, newRepo)
+
if err != nil {
+
l.Error("failed to update repository", "err", err)
+
rp.pages.Notice(w, noticeId, "Failed to save repository information.")
+
return
+
}
+
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey)
+
if err != nil {
+
// failed to get record
+
l.Error("failed to get repo record", "err", err)
+
rp.pages.Notice(w, noticeId, "Failed to save repository information, no record found on PDS.")
+
return
+
}
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+
Collection: tangled.RepoNSID,
+
Repo: newRepo.Did,
+
Rkey: newRepo.Rkey,
+
SwapRecord: ex.Cid,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &record,
+
},
+
})
+
+
if err != nil {
+
l.Error("failed to perferom update-repo query", "err", err)
+
// failed to get record
+
rp.pages.Notice(w, noticeId, "Failed to save repository information, unable to save to PDS.")
+
return
+
}
+
+
err = tx.Commit()
+
if err != nil {
+
l.Error("failed to commit", "err", err)
+
}
+
+
rp.pages.HxRefresh(w)
+
}
+79
appview/repo/tags.go
···
+
package repo
+
+
import (
+
"encoding/json"
+
"fmt"
+
"net/http"
+
+
"tangled.org/core/api/tangled"
+
"tangled.org/core/appview/db"
+
"tangled.org/core/appview/models"
+
"tangled.org/core/appview/pages"
+
xrpcclient "tangled.org/core/appview/xrpcclient"
+
"tangled.org/core/types"
+
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+
"github.com/go-git/go-git/v5/plumbing"
+
)
+
+
func (rp *Repo) Tags(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "RepoTags")
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to get repo and knot", "err", err)
+
return
+
}
+
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 xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.tags", "err", xrpcerr)
+
rp.pages.Error503(w)
+
return
+
}
+
var result types.RepoTagsResponse
+
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
+
l.Error("failed to decode XRPC response", "err", err)
+
rp.pages.Error503(w)
+
return
+
}
+
artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt()))
+
if err != nil {
+
l.Error("failed grab artifacts", "err", err)
+
return
+
}
+
// convert artifacts to map for easy UI building
+
artifactMap := make(map[plumbing.Hash][]models.Artifact)
+
for _, a := range artifacts {
+
artifactMap[a.Tag] = append(artifactMap[a.Tag], a)
+
}
+
var danglingArtifacts []models.Artifact
+
for _, a := range artifacts {
+
found := false
+
for _, t := range result.Tags {
+
if t.Tag != nil {
+
if t.Tag.Hash == a.Tag {
+
found = true
+
}
+
}
+
}
+
if !found {
+
danglingArtifacts = append(danglingArtifacts, a)
+
}
+
}
+
user := rp.oauth.GetUser(r)
+
rp.pages.RepoTags(w, pages.RepoTagsParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(user),
+
RepoTagsResponse: result,
+
ArtifactMap: artifactMap,
+
DanglingArtifacts: danglingArtifacts,
+
})
+
}
+107
appview/repo/tree.go
···
+
package repo
+
+
import (
+
"fmt"
+
"net/http"
+
"net/url"
+
"strings"
+
"time"
+
+
"tangled.org/core/api/tangled"
+
"tangled.org/core/appview/pages"
+
xrpcclient "tangled.org/core/appview/xrpcclient"
+
"tangled.org/core/types"
+
+
indigoxrpc "github.com/bluesky-social/indigo/xrpc"
+
"github.com/go-chi/chi/v5"
+
"github.com/go-git/go-git/v5/plumbing"
+
)
+
+
func (rp *Repo) Tree(w http.ResponseWriter, r *http.Request) {
+
l := rp.logger.With("handler", "RepoTree")
+
f, err := rp.repoResolver.Resolve(r)
+
if err != nil {
+
l.Error("failed to fully resolve repo", "err", err)
+
return
+
}
+
ref := chi.URLParam(r, "ref")
+
ref, _ = url.PathUnescape(ref)
+
// if the tree path has a trailing slash, let's strip it
+
// so we don't 404
+
treePath := chi.URLParam(r, "*")
+
treePath, _ = url.PathUnescape(treePath)
+
treePath = strings.TrimSuffix(treePath, "/")
+
scheme := "http"
+
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)
+
xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
+
if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
+
l.Error("failed to call XRPC repo.tree", "err", xrpcerr)
+
rp.pages.Error503(w)
+
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
+
}
+
result := types.RepoTreeResponse{
+
Ref: xrpcResp.Ref,
+
Files: files,
+
}
+
if xrpcResp.Parent != nil {
+
result.Parent = *xrpcResp.Parent
+
}
+
if xrpcResp.Dotdot != nil {
+
result.DotDot = *xrpcResp.Dotdot
+
}
+
if xrpcResp.Readme != nil {
+
result.ReadmeFileName = xrpcResp.Readme.Filename
+
result.Readme = xrpcResp.Readme.Contents
+
}
+
// redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
+
// so we can safely redirect to the "parent" (which is the same file).
+
if len(result.Files) == 0 && result.Parent == treePath {
+
redirectTo := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), url.PathEscape(ref), result.Parent)
+
http.Redirect(w, r, redirectTo, http.StatusFound)
+
return
+
}
+
user := rp.oauth.GetUser(r)
+
var breadcrumbs [][]string
+
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
+
if treePath != "" {
+
for idx, elem := range strings.Split(treePath, "/") {
+
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
+
}
+
}
+
sortFiles(result.Files)
+
rp.pages.RepoTree(w, pages.RepoTreeParams{
+
LoggedInUser: user,
+
BreadCrumbs: breadcrumbs,
+
TreePath: treePath,
+
RepoInfo: f.RepoInfo(user),
+
RepoTreeResponse: result,
+
})
+
}