···
"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/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"
···
-
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)
-
l.Error("failed to get repo and knot", "err", err)
-
if !rp.config.Core.Dev {
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
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)
-
// 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
-
func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoLog")
-
f, err := rp.repoResolver.Resolve(r)
-
l.Error("failed to fully resolve repo", "err", err)
-
if r.URL.Query().Get("page") != "" {
-
page, err = strconv.Atoi(r.URL.Query().Get("page"))
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
if !rp.config.Core.Dev {
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
// 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)
-
var xrpcResp types.RepoLogResponse
-
if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil {
-
l.Error("failed to decode XRPC response", "err", err)
-
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)
-
tagMap := make(map[string][]string)
-
var tagResp types.RepoTagsResponse
-
if err := json.Unmarshal(tagBytes, &tagResp); err == nil {
-
for _, tag := range tagResp.Tags {
-
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)
-
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)
-
l.Error("failed to fetch email to did mapping", "err", err)
-
vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits)
-
l.Error("failed to GetVerifiedObjectCommits", "err", err)
-
repoInfo := f.RepoInfo(user)
-
for _, c := range xrpcResp.Commits {
-
shas = append(shas, c.Hash.String())
-
pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas)
-
l.Error("failed to getPipelineStatuses", "err", err)
-
rp.pages.RepoLog(w, pages.RepoLogParams{
-
RepoLogResponse: xrpcResp,
-
EmailToDid: emailToDidMap,
-
func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoCommit")
-
f, err := rp.repoResolver.Resolve(r)
-
l.Error("failed to fully resolve repo", "err", err)
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
var diffOpts types.DiffOpts
-
if d := r.URL.Query().Get("diff"); d == "split" {
-
if !plumbing.IsHash(ref) {
-
if !rp.config.Core.Dev {
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
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)
-
var result types.RepoCommitResponse
-
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
-
l.Error("failed to decode XRPC response", "err", err)
-
emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Committer.Email, result.Diff.Commit.Author.Email}, true)
-
l.Error("failed to get email to did mapping", "err", err)
-
vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff})
-
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})
-
l.Error("failed to getPipelineStatuses", "err", err)
-
var pipeline *models.Pipeline
-
if p, ok := pipelines[result.Diff.Commit.This]; ok {
-
rp.pages.RepoCommit(w, pages.RepoCommitParams{
-
RepoInfo: f.RepoInfo(user),
-
RepoCommitResponse: result,
-
EmailToDid: emailToDidMap,
-
func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoTree")
-
f, err := rp.repoResolver.Resolve(r)
-
l.Error("failed to fully resolve repo", "err", err)
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
// if the tree path has a trailing slash, let's strip it
-
treePath := chi.URLParam(r, "*")
-
treePath, _ = url.PathUnescape(treePath)
-
treePath = strings.TrimSuffix(treePath, "/")
-
if !rp.config.Core.Dev {
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
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)
-
// Convert XRPC response to internal types.RepoTreeResponse
-
files := make([]types.NiceTree, len(xrpcResp.Files))
-
for i, xrpcFile := range xrpcResp.Files {
-
file := types.NiceTree{
-
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,
-
result := types.RepoTreeResponse{
-
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)
-
user := rp.oauth.GetUser(r)
-
var breadcrumbs [][]string
-
breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
-
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{
-
BreadCrumbs: breadcrumbs,
-
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)
-
l.Error("failed to get repo and knot", "err", err)
-
if !rp.config.Core.Dev {
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
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)
-
var result types.RepoTagsResponse
-
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
-
l.Error("failed to decode XRPC response", "err", err)
-
artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt()))
-
l.Error("failed grab artifacts", "err", err)
-
// 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 {
-
for _, t := range result.Tags {
-
if t.Tag.Hash == a.Tag {
-
danglingArtifacts = append(danglingArtifacts, a)
-
user := rp.oauth.GetUser(r)
-
rp.pages.RepoTags(w, pages.RepoTagsParams{
-
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)
-
l.Error("failed to get repo and knot", "err", err)
-
if !rp.config.Core.Dev {
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
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)
-
var result types.RepoBranchesResponse
-
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
-
l.Error("failed to decode XRPC response", "err", err)
-
sortBranches(result.Branches)
-
user := rp.oauth.GetUser(r)
-
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
-
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)
-
l.Error("failed to get repo and knot", "err", err)
-
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")
-
fail("No branch provided.", nil)
-
client, err := rp.oauth.ServiceClient(
-
oauth.WithService(f.Knot),
-
oauth.WithLxm(tangled.RepoDeleteBranchNSID),
-
oauth.WithDev(rp.config.Core.Dev),
-
fail("Failed to connect to knotserver", nil)
-
err = tangled.RepoDeleteBranch(
-
&tangled.RepoDeleteBranch_Input{
-
Repo: f.RepoAt().String(),
-
if err := xrpcclient.HandleXrpcErr(err); err != nil {
-
fail(fmt.Sprintf("Failed to delete branch: %s", err), err)
-
l.Error("deleted branch from knot", "branch", branch, "repo", f.RepoAt())
-
func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoBlob")
-
f, err := rp.repoResolver.Resolve(r)
-
l.Error("failed to get repo and knot", "err", err)
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
filePath := chi.URLParam(r, "*")
-
filePath, _ = url.PathUnescape(filePath)
-
if !rp.config.Core.Dev {
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
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)
-
// 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))})
-
for idx, elem := range strings.Split(filePath, "/") {
-
breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
-
if markup.GetFormat(resp.Path) == markup.FormatMarkdown {
-
showRendered = r.URL.Query().Get("code") != "true"
-
if resp.IsBinary != nil && *resp.IsBinary {
-
ext := strings.ToLower(filepath.Ext(resp.Path))
-
case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp":
-
case ".mp4", ".webm", ".ogg", ".mov", ".avi":
-
// fetch the raw binary content using sh.tangled.repo.blob xrpc
-
repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name)
-
Path: "/xrpc/sh.tangled.repo.blob",
-
query := baseURL.Query()
-
query.Set("repo", repoName)
-
query.Set("path", filePath)
-
query.Set("raw", "true")
-
baseURL.RawQuery = query.Encode()
-
blobURL := baseURL.String()
-
if !rp.config.Core.Dev {
-
contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL)
-
if resp.IsBinary == nil || !*resp.IsBinary {
-
lines = strings.Count(resp.Content, "\n") + 1
-
sizeHint = uint64(*resp.Size)
-
sizeHint = uint64(len(resp.Content))
-
user := rp.oauth.GetUser(r)
-
// Determine if content is binary (dereference pointer)
-
if resp.IsBinary != nil {
-
isBinary = *resp.IsBinary
-
rp.pages.RepoBlob(w, pages.RepoBlobParams{
-
RepoInfo: f.RepoInfo(user),
-
BreadCrumbs: breadcrumbs,
-
ShowRendered: showRendered,
-
RenderToggle: renderToggle,
-
Unsupported: unsupported,
-
ContentSrc: contentSrc,
-
Contents: resp.Content,
-
func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "RepoBlobRaw")
-
f, err := rp.repoResolver.Resolve(r)
-
l.Error("failed to get repo and knot", "err", err)
-
w.WriteHeader(http.StatusBadRequest)
-
ref := chi.URLParam(r, "ref")
-
ref, _ = url.PathUnescape(ref)
-
filePath := chi.URLParam(r, "*")
-
filePath, _ = url.PathUnescape(filePath)
-
if !rp.config.Core.Dev {
-
repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name)
-
Path: "/xrpc/sh.tangled.repo.blob",
-
query := baseURL.Query()
-
query.Set("repo", repo)
-
query.Set("path", filePath)
-
query.Set("raw", "true")
-
baseURL.RawQuery = query.Encode()
-
blobURL := baseURL.String()
-
req, err := http.NewRequest("GET", blobURL, nil)
-
l.Error("failed to create request", "err", err)
-
// 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)
-
l.Error("failed to reach knotserver", "err", err)
-
defer resp.Body.Close()
-
// forward 304 not modified
-
if resp.StatusCode == http.StatusNotModified {
-
w.WriteHeader(http.StatusNotModified)
-
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)
-
contentType := resp.Header.Get("Content-Type")
-
body, err := io.ReadAll(resp.Body)
-
l.Error("error reading response body from knotserver", "err", err)
-
w.WriteHeader(http.StatusInternalServerError)
-
if strings.HasPrefix(contentType, "text/") || isTextualMimeType(contentType) {
-
// serve all textual content as text/plain
-
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
-
} 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.WriteHeader(http.StatusUnsupportedMediaType)
-
w.Write([]byte("unsupported content type"))
// 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/javascript",
-
"application/ecmascript",
-
return slices.Contains(textualTypes, mimeType)
// modify the spindle configured for this repo
func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) {
···
SubjectDid: collaboratorIdent.DID,
-
fail("Failed to add collaborator.", err)
-
fail("Failed to add collaborator.", err)
-
err = rp.enforcer.E.SavePolicy()
-
fail("Failed to update collaborator permissions.", err)
-
// clear aturi to when everything is successful
-
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)
-
l.Error("failed to get repo and knot", "err", err)
-
// remove record from pds
-
atpClient, err := rp.oauth.AuthorizedClient(r)
-
l.Error("failed to get authorized client", "err", err)
-
_, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{
-
Collection: tangled.RepoNSID,
-
l.Error("failed to delete record", "err", err)
-
rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.")
-
l.Info("removed repo record", "aturi", f.RepoAt().String())
-
client, err := rp.oauth.ServiceClient(
-
oauth.WithService(f.Knot),
-
oauth.WithLxm(tangled.RepoDeleteNSID),
-
oauth.WithDev(rp.config.Core.Dev),
-
l.Error("failed to connect to knot server", "err", err)
-
err = tangled.RepoDelete(
-
&tangled.RepoDelete_Input{
-
if err := xrpcclient.HandleXrpcErr(err); err != nil {
-
rp.pages.Notice(w, noticeId, err.Error())
-
l.Info("deleted repo from knot")
-
tx, err := rp.db.BeginTx(r.Context(), nil)
-
l.Error("failed to start tx")
-
w.Write(fmt.Append(nil, "failed to add collaborator: ", err))
-
err = rp.enforcer.E.LoadPolicy()
-
l.Error("failed to rollback policies")
-
// remove collaborator RBAC
-
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
-
rp.pages.Notice(w, noticeId, "Failed to remove collaborators")
-
for _, c := range repoCollaborators {
-
rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
-
l.Info("removed collaborators")
-
err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
-
rp.pages.Notice(w, noticeId, "Failed to update RBAC rules")
-
err = db.RemoveRepo(tx, f.OwnerDid(), f.Name)
-
rp.pages.Notice(w, noticeId, "Failed to update appview")
-
l.Info("removed repo from db")
-
l.Error("failed to commit changes", "err", err)
-
http.Error(w, err.Error(), http.StatusInternalServerError)
-
err = rp.enforcer.E.SavePolicy()
-
l.Error("failed to update ACLs", "err", err)
-
http.Error(w, err.Error(), http.StatusInternalServerError)
-
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)
-
l.Error("failed to get repo and knot", "err", err)
-
w.WriteHeader(http.StatusBadRequest)
-
client, err := rp.oauth.AuthorizedClient(r)
-
l.Error("failed to get client")
-
rp.pages.Notice(w, noticeId, "Failed to update repository information, try again later.")
-
description = r.FormValue("description")
-
website = r.FormValue("website")
-
topicStr = r.FormValue("topics")
-
err = rp.validator.ValidateURI(website)
-
l.Error("invalid uri", "err", err)
-
rp.pages.Notice(w, noticeId, err.Error())
-
topics, err := rp.validator.ValidateRepoTopicStr(topicStr)
-
l.Error("invalid topics", "err", err)
-
rp.pages.Notice(w, noticeId, err.Error())
-
l.Debug("got", "topicsStr", topicStr, "topics", topics)
-
newRepo.Description = description
-
newRepo.Website = website
-
newRepo.Topics = topics
-
record := newRepo.AsRecord()
-
tx, err := rp.db.BeginTx(r.Context(), nil)
-
l.Error("failed to begin transaction", "err", err)
-
rp.pages.Notice(w, noticeId, "Failed to save repository information.")
-
err = db.PutRepo(tx, newRepo)
-
l.Error("failed to update repository", "err", err)
-
rp.pages.Notice(w, noticeId, "Failed to save repository information.")
-
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey)
-
// 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.")
-
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
-
Collection: tangled.RepoNSID,
-
Record: &lexutil.LexiconTypeDecoder{
-
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.")
-
l.Error("failed to commit", "err", err)
-
func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
-
l := rp.logger.With("handler", "SetDefaultBranch")
f, err := rp.repoResolver.Resolve(r)
l.Error("failed to get repo and knot", "err", err)
-
noticeId := "operation-error"
-
branch := r.FormValue("branch")
-
http.Error(w, "malformed form", http.StatusBadRequest)
client, err := rp.oauth.ServiceClient(
oauth.WithService(f.Knot),
-
oauth.WithLxm(tangled.RepoSetDefaultBranchNSID),
oauth.WithDev(rp.config.Core.Dev),
l.Error("failed to connect to knot server", "err", err)
-
rp.pages.Notice(w, noticeId, "Failed to connect to knot server.")
-
xe := tangled.RepoSetDefaultBranch(
-
&tangled.RepoSetDefaultBranch_Input{
-
Repo: f.RepoAt().String(),
-
if err := xrpcclient.HandleXrpcErr(xe); err != nil {
-
l.Error("xrpc failed", "err", xe)
rp.pages.Notice(w, noticeId, err.Error())
-
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)
-
l.Error("failed to get repo and knot", "err", err)
-
l.Error("empty spindle cannot add/rm secret", "err", err)
-
lxm := tangled.RepoAddSecretNSID
-
if r.Method == http.MethodDelete {
-
lxm = tangled.RepoRemoveSecretNSID
-
spindleClient, err := rp.oauth.ServiceClient(
-
oauth.WithService(f.Spindle),
-
oauth.WithDev(rp.config.Core.Dev),
-
l.Error("failed to create spindle client", "err", err)
-
key := r.FormValue("key")
-
w.WriteHeader(http.StatusBadRequest)
-
errorId := "add-secret-error"
-
value := r.FormValue("value")
-
w.WriteHeader(http.StatusBadRequest)
-
err = tangled.RepoAddSecret(
-
&tangled.RepoAddSecret_Input{
-
Repo: f.RepoAt().String(),
-
l.Error("Failed to add secret.", "err", err)
-
rp.pages.Notice(w, errorId, "Failed to add secret.")
-
case http.MethodDelete:
-
errorId := "operation-error"
-
err = tangled.RepoRemoveSecret(
-
&tangled.RepoRemoveSecret_Input{
-
Repo: f.RepoAt().String(),
-
l.Error("Failed to delete secret.", "err", err)
-
rp.pages.Notice(w, errorId, "Failed to delete secret.")
-
type tab = map[string]any
-
// 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")
-
rp.generalSettings(w, r)
-
rp.accessSettings(w, r)
-
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)
-
if !rp.config.Core.Dev {
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
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)
-
var result types.RepoBranchesResponse
-
if err := json.Unmarshal(xrpcBytes, &result); err != nil {
-
l.Error("failed to decode XRPC response", "err", err)
-
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
-
l.Error("failed to fetch labels", "err", err)
-
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels))
-
l.Error("failed to fetch labels", "err", err)
-
// remove default labels from the labels list, if present
-
defaultLabelMap := make(map[string]bool)
-
for _, dl := range defaultLabels {
-
defaultLabelMap[dl.AtUri().String()] = true
-
for _, l := range labels {
-
if !defaultLabelMap[l.AtUri().String()] {
-
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
-
rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{
-
RepoInfo: f.RepoInfo(user),
-
Branches: result.Branches,
-
DefaultLabels: defaultLabels,
-
SubscribedLabels: subscribedLabels,
-
ShouldSubscribeAll: shouldSubscribeAll,
-
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())
-
l.Error("failed to get collaborators", "err", err)
-
rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{
-
RepoInfo: f.RepoInfo(user),
-
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())
-
l.Error("failed to fetch spindles", "err", err)
-
var secrets []*tangled.RepoListSecrets_Secret
-
if spindleClient, err := rp.oauth.ServiceClient(
-
oauth.WithService(f.Spindle),
-
oauth.WithLxm(tangled.RepoListSecretsNSID),
-
oauth.WithDev(rp.config.Core.Dev),
-
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)
-
slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int {
-
return strings.Compare(a.Key, b.Key)
-
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{
-
"CreatedBy": resolvedIdents[id].Handle.String(),
-
rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{
-
RepoInfo: f.RepoInfo(user),
-
CurrentSpindle: f.Spindle,
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
···
-
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)
-
l.Error("failed to get repo and knot", "err", err)
-
if !rp.config.Core.Dev {
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
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)
-
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.")
-
branches := branchResult.Branches
-
var defaultBranch string
-
for _, b := range branches {
-
params := r.URL.Query()
-
queryBase := params.Get("base")
-
queryHead := params.Get("head")
-
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)
-
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.")
-
repoinfo := f.RepoInfo(user)
-
rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{
-
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)
-
l.Error("failed to get repo and knot", "err", err)
-
var diffOpts types.DiffOpts
-
if d := r.URL.Query().Get("diff"); d == "split" {
-
// 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)
-
base, _ = url.PathUnescape(base)
-
head, _ = url.PathUnescape(head)
-
if base == "" || head == "" {
-
l.Error("invalid comparison")
-
if !rp.config.Core.Dev {
-
host := fmt.Sprintf("%s://%s", scheme, f.Knot)
-
xrpcc := &indigoxrpc.Client{
-
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)
-
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.")
-
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)
-
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.")
-
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)
-
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.")
-
var diff types.NiceDiff
-
if formatPatch.CombinedPatchRaw != "" {
-
diff = patchutil.AsNiceDiff(formatPatch.CombinedPatchRaw, base)
-
diff = patchutil.AsNiceDiff(formatPatch.FormatPatchRaw, base)
-
repoinfo := f.RepoInfo(user)
-
rp.pages.RepoCompare(w, pages.RepoCompareParams{
-
Branches: branches.Branches,