···
-
"go.opentelemetry.io/otel/attribute"
-
"go.opentelemetry.io/otel/codes"
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/auth"
···
func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoIndex")
ref := chi.URLParam(r, "ref")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to fully resolve repo", err)
-
span.SetStatus(codes.Error, "failed to fully resolve repo")
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
log.Printf("failed to create unsigned client for %s", f.Knot)
-
span.SetStatus(codes.Error, "failed to create unsigned client")
···
log.Println("failed to reach knotserver", err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
···
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
-
span.SetStatus(codes.Error, "error reading response body")
···
err = json.Unmarshal(body, &result)
log.Printf("Error unmarshalling response body: %v", err)
-
span.SetStatus(codes.Error, "error unmarshalling response body")
···
tagCount := len(result.Tags)
fileCount := len(result.Files)
-
attribute.Int("commits.count", commitCount),
-
attribute.Int("branches.count", branchCount),
-
attribute.Int("tags.count", tagCount),
-
attribute.Int("files.count", fileCount),
commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount)
commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))]
tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))]
···
user := s.auth.GetUser(r)
s.pages.RepoIndexPage(w, pages.RepoIndexParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoIndexResponse: result,
CommitsTrunc: commitsTrunc,
···
func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoLog")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to fully resolve repo", err)
-
span.SetStatus(codes.Error, "failed to fully resolve repo")
···
ref := chi.URLParam(r, "ref")
-
span.SetAttributes(attribute.Int("page", page), attribute.String("ref", ref))
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
log.Println("failed to create unsigned client", err)
-
span.SetStatus(codes.Error, "failed to create unsigned client")
resp, err := us.Log(f.OwnerDid(), f.RepoName, ref, page)
log.Println("failed to reach knotserver", err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
body, err := io.ReadAll(resp.Body)
log.Printf("error reading response body: %v", err)
-
span.SetStatus(codes.Error, "error reading response body")
···
err = json.Unmarshal(body, &repolog)
log.Println("failed to parse json response", err)
-
span.SetStatus(codes.Error, "failed to parse json response")
-
span.SetAttributes(attribute.Int("commits.count", len(repolog.Commits)))
result, err := us.Tags(f.OwnerDid(), f.RepoName)
log.Println("failed to reach knotserver", err)
-
span.SetStatus(codes.Error, "failed to reach knotserver for tags")
···
tagMap[hash] = append(tagMap[hash], tag.Name)
-
span.SetAttributes(attribute.Int("tags.count", len(result.Tags)))
user := s.auth.GetUser(r)
s.pages.RepoLog(w, pages.RepoLogParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoLogResponse: repolog,
EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)),
···
func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoDescriptionEdit")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
w.WriteHeader(http.StatusBadRequest)
···
user := s.auth.GetUser(r)
s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoDescription")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
w.WriteHeader(http.StatusBadRequest)
···
rkey := repoAt.RecordKey().String()
log.Println("invalid aturi for repo", err)
-
span.SetStatus(codes.Error, "invalid aturi for repo")
w.WriteHeader(http.StatusInternalServerError)
user := s.auth.GetUser(r)
-
span.SetAttributes(attribute.String("method", r.Method))
s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
user := s.auth.GetUser(r)
newDescription := r.FormValue("description")
-
span.SetAttributes(attribute.String("description", newDescription))
client, _ := s.auth.AuthorizedClient(r)
-
err = db.UpdateDescription(ctx, s.db, string(repoAt), newDescription)
-
log.Println("failed to perform update-description query", err)
-
span.SetStatus(codes.Error, "failed to update description in database")
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
// this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field
// SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests
-
ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoNSID, user.Did, rkey)
-
span.SetStatus(codes.Error, "failed to get record from PDS")
s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
-
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
···
-
log.Println("failed to perform update-description query", err)
-
span.SetStatus(codes.Error, "failed to put record to PDS")
s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.")
-
newRepoInfo := f.RepoInfo(ctx, s, user)
newRepoInfo.Description = newDescription
s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
···
func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoCommit")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to fully resolve repo", err)
-
span.SetStatus(codes.Error, "failed to fully resolve repo")
ref := chi.URLParam(r, "ref")
···
-
span.SetAttributes(attribute.String("ref", ref), attribute.String("protocol", protocol))
if !plumbing.IsHash(ref) {
-
span.SetAttributes(attribute.Bool("invalid_hash", true))
-
requestURL := fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref)
-
span.SetAttributes(attribute.String("request_url", requestURL))
-
resp, err := http.Get(requestURL)
log.Println("failed to reach knotserver", err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
-
span.SetStatus(codes.Error, "error reading response body")
···
err = json.Unmarshal(body, &result)
log.Println("failed to parse response:", err)
-
span.SetStatus(codes.Error, "failed to parse response")
user := s.auth.GetUser(r)
s.pages.RepoCommit(w, pages.RepoCommitParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoCommitResponse: result,
EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}),
···
func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoTree")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to fully resolve repo", err)
-
span.SetStatus(codes.Error, "failed to fully resolve repo")
···
-
attribute.String("ref", ref),
-
attribute.String("tree_path", treePath),
-
attribute.String("protocol", protocol),
-
requestURL := fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)
-
span.SetAttributes(attribute.String("request_url", requestURL))
-
resp, err := http.Get(requestURL)
log.Println("failed to reach knotserver", err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
-
span.SetStatus(codes.Error, "error reading response body")
···
err = json.Unmarshal(body, &result)
log.Println("failed to parse response:", err)
-
span.SetStatus(codes.Error, "failed to parse response")
// 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 {
-
redirectURL := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent)
-
span.SetAttributes(attribute.String("redirect_url", redirectURL))
-
http.Redirect(w, r, redirectURL, http.StatusFound)
···
BreadCrumbs: breadcrumbs,
BaseTreeLink: baseTreeLink,
BaseBlobLink: baseBlobLink,
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoTreeResponse: result,
func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoTags")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
log.Println("failed to create unsigned client", err)
-
span.SetStatus(codes.Error, "failed to create unsigned client")
result, err := us.Tags(f.OwnerDid(), f.RepoName)
log.Println("failed to reach knotserver", err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
-
span.SetAttributes(attribute.Int("tags.count", len(result.Tags)))
artifacts, err := db.GetArtifact(s.db, db.Filter("repo_at", f.RepoAt))
log.Println("failed grab artifacts", err)
-
span.SetStatus(codes.Error, "failed to grab artifacts")
-
span.SetAttributes(attribute.Int("artifacts.count", len(artifacts)))
// convert artifacts to map for easy UI building
artifactMap := make(map[plumbing.Hash][]db.Artifact)
···
-
span.SetAttributes(attribute.Int("dangling_artifacts.count", len(danglingArtifacts)))
user := s.auth.GetUser(r)
s.pages.RepoTags(w, pages.RepoTagsParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoTagsResponse: *result,
ArtifactMap: artifactMap,
DanglingArtifacts: danglingArtifacts,
···
func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoBranches")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
log.Println("failed to create unsigned client", err)
-
span.SetStatus(codes.Error, "failed to create unsigned client")
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
log.Println("failed to reach knotserver", err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
-
span.SetStatus(codes.Error, "error reading response body")
···
err = json.Unmarshal(body, &result)
log.Println("failed to parse response:", err)
-
span.SetStatus(codes.Error, "failed to parse response")
-
span.SetAttributes(attribute.Int("branches.count", len(result.Branches)))
slices.SortFunc(result.Branches, func(a, b types.Branch) int {
···
user := s.auth.GetUser(r)
s.pages.RepoBranches(w, pages.RepoBranchesParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoBranchesResponse: result,
func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoBlob")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
···
-
attribute.String("ref", ref),
-
attribute.String("file_path", filePath),
-
attribute.String("protocol", protocol),
-
requestURL := fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)
-
span.SetAttributes(attribute.String("request_url", requestURL))
-
resp, err := http.Get(requestURL)
log.Println("failed to reach knotserver", err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
-
span.SetStatus(codes.Error, "error reading response body")
···
err = json.Unmarshal(body, &result)
log.Println("failed to parse response:", err)
-
span.SetStatus(codes.Error, "failed to parse response")
···
showRendered = r.URL.Query().Get("code") != "true"
-
attribute.Bool("is_binary", result.IsBinary),
-
attribute.Bool("show_rendered", showRendered),
-
attribute.Bool("render_toggle", renderToggle),
user := s.auth.GetUser(r)
s.pages.RepoBlob(w, pages.RepoBlobParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
RepoBlobResponse: result,
BreadCrumbs: breadcrumbs,
ShowRendered: showRendered,
···
func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoBlobRaw")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
···
-
attribute.String("ref", ref),
-
attribute.String("file_path", filePath),
-
attribute.String("protocol", protocol),
-
requestURL := fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)
-
span.SetAttributes(attribute.String("request_url", requestURL))
-
resp, err := http.Get(requestURL)
log.Println("failed to reach knotserver", err)
-
span.SetStatus(codes.Error, "failed to reach knotserver")
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
-
span.SetStatus(codes.Error, "error reading response body")
···
err = json.Unmarshal(body, &result)
log.Println("failed to parse response:", err)
-
span.SetStatus(codes.Error, "failed to parse response")
-
span.SetAttributes(attribute.Bool("is_binary", result.IsBinary))
w.Header().Set("Content-Type", "application/octet-stream")
···
func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "AddCollaborator")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
collaborator := r.FormValue("collaborator")
-
span.SetAttributes(attribute.String("error", "malformed_form"))
http.Error(w, "malformed form", http.StatusBadRequest)
-
span.SetAttributes(attribute.String("collaborator", collaborator))
-
collaboratorIdent, err := s.resolver.ResolveIdent(ctx, collaborator)
-
span.SetStatus(codes.Error, "failed to resolve collaborator")
w.Write([]byte("failed to resolve collaborator did to a handle"))
log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
-
attribute.String("collaborator_did", collaboratorIdent.DID.String()),
-
attribute.String("collaborator_handle", collaboratorIdent.Handle.String()),
// TODO: create an atproto record for this
secret, err := db.GetRegistrationKey(s.db, f.Knot)
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
-
span.SetStatus(codes.Error, "no key found for domain")
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
log.Println("failed to create client to ", f.Knot)
-
span.SetStatus(codes.Error, "failed to create signed client")
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
log.Printf("failed to make request to %s: %s", f.Knot, err)
-
span.SetStatus(codes.Error, "failed to make request to knotserver")
if ksResp.StatusCode != http.StatusNoContent {
-
span.SetAttributes(attribute.Int("status_code", ksResp.StatusCode))
w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
-
tx, err := s.db.BeginTx(ctx, nil)
log.Println("failed to start tx")
-
span.SetStatus(codes.Error, "failed to start transaction")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
···
err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
-
span.SetStatus(codes.Error, "failed to add collaborator to enforcer")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
-
err = db.AddCollaborator(ctx, s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
-
span.SetStatus(codes.Error, "failed to add collaborator to database")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
···
log.Println("failed to commit changes", err)
-
span.SetStatus(codes.Error, "failed to commit transaction")
http.Error(w, err.Error(), http.StatusInternalServerError)
···
err = s.enforcer.E.SavePolicy()
log.Println("failed to update ACLs", err)
-
span.SetStatus(codes.Error, "failed to save enforcer policy")
http.Error(w, err.Error(), http.StatusInternalServerError)
w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "DeleteRepo")
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
-
attribute.String("repo_name", f.RepoName),
-
attribute.String("knot", f.Knot),
-
attribute.String("owner_did", f.OwnerDid()),
// remove record from pds
xrpcClient, _ := s.auth.AuthorizedClient(r)
repoRkey := f.RepoAt.RecordKey().String()
-
_, err = comatproto.RepoDeleteRecord(ctx, xrpcClient, &comatproto.RepoDeleteRecord_Input{
Collection: tangled.RepoNSID,
log.Printf("failed to delete record: %s", err)
-
span.SetStatus(codes.Error, "failed to delete record from PDS")
s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.")
log.Println("removed repo record ", f.RepoAt.String())
-
span.SetAttributes(attribute.String("repo_at", f.RepoAt.String()))
secret, err := db.GetRegistrationKey(s.db, f.Knot)
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
-
span.SetStatus(codes.Error, "no key found for domain")
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
log.Println("failed to create client to ", f.Knot)
-
span.SetStatus(codes.Error, "failed to create client")
ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName)
log.Printf("failed to make request to %s: %s", f.Knot, err)
-
span.SetStatus(codes.Error, "failed to make request to knotserver")
-
span.SetAttributes(attribute.Int("ks_status_code", ksResp.StatusCode))
if ksResp.StatusCode != http.StatusNoContent {
log.Println("failed to remove repo from knot, continuing anyway ", f.Knot)
-
span.SetAttributes(attribute.Bool("knot_remove_failed", true))
log.Println("removed repo from knot ", f.Knot)
-
span.SetAttributes(attribute.Bool("knot_remove_success", true))
-
tx, err := s.db.BeginTx(ctx, nil)
log.Println("failed to start tx")
-
span.SetStatus(codes.Error, "failed to start transaction")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
···
err = s.enforcer.E.LoadPolicy()
log.Println("failed to rollback policies")
// remove collaborator RBAC
repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
-
span.SetStatus(codes.Error, "failed to get collaborators")
s.pages.Notice(w, "settings-delete", "Failed to remove collaborators")
-
span.SetAttributes(attribute.Int("collaborators.count", len(repoCollaborators)))
for _, c := range repoCollaborators {
s.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
···
err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
-
span.SetStatus(codes.Error, "failed to remove repo RBAC")
s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules")
-
err = db.RemoveRepo(ctx, tx, f.OwnerDid(), f.RepoName)
-
span.SetStatus(codes.Error, "failed to remove repo from db")
s.pages.Notice(w, "settings-delete", "Failed to update appview")
···
log.Println("failed to commit changes", err)
-
span.SetStatus(codes.Error, "failed to commit transaction")
http.Error(w, err.Error(), http.StatusInternalServerError)
···
err = s.enforcer.E.SavePolicy()
log.Println("failed to update ACLs", err)
-
span.SetStatus(codes.Error, "failed to save policy")
http.Error(w, err.Error(), http.StatusInternalServerError)
···
func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "SetDefaultBranch")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
branch := r.FormValue("branch")
-
span.SetAttributes(attribute.Bool("malformed_form", true))
-
span.SetStatus(codes.Error, "malformed form")
http.Error(w, "malformed form", http.StatusBadRequest)
-
attribute.String("branch", branch),
-
attribute.String("repo_name", f.RepoName),
-
attribute.String("knot", f.Knot),
-
attribute.String("owner_did", f.OwnerDid()),
secret, err := db.GetRegistrationKey(s.db, f.Knot)
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
-
span.SetStatus(codes.Error, "no key found for domain")
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
log.Println("failed to create client to ", f.Knot)
-
span.SetStatus(codes.Error, "failed to create client")
ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch)
log.Printf("failed to make request to %s: %s", f.Knot, err)
-
span.SetStatus(codes.Error, "failed to make request to knotserver")
-
span.SetAttributes(attribute.Int("ks_status_code", ksResp.StatusCode))
if ksResp.StatusCode != http.StatusNoContent {
-
span.SetStatus(codes.Error, "failed to set default branch")
s.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.")
···
func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoSettings")
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to get repo and knot")
-
attribute.String("repo_name", f.RepoName),
-
attribute.String("knot", f.Knot),
-
attribute.String("owner_did", f.OwnerDid()),
-
attribute.String("method", r.Method),
// for now, this is just pubkeys
user := s.auth.GetUser(r)
-
repoCollaborators, err := f.Collaborators(ctx, s)
log.Println("failed to get collaborators", err)
-
span.SetAttributes(attribute.String("error", "failed_to_get_collaborators"))
-
span.SetAttributes(attribute.Int("collaborators.count", len(repoCollaborators)))
isCollaboratorInviteAllowed := false
···
isCollaboratorInviteAllowed = true
-
span.SetAttributes(attribute.Bool("invite_allowed", isCollaboratorInviteAllowed))
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
log.Println("failed to create unsigned client", err)
-
span.SetAttributes(attribute.String("error", "failed_to_create_unsigned_client"))
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
log.Println("failed to reach knotserver", err)
-
span.SetAttributes(attribute.String("error", "failed_to_reach_knotserver_for_branches"))
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
-
span.SetAttributes(attribute.String("error", "failed_to_read_branches_response"))
var result types.RepoBranchesResponse
err = json.Unmarshal(body, &result)
log.Println("failed to parse response:", err)
-
span.SetAttributes(attribute.String("error", "failed_to_parse_branches_response"))
for _, branch := range result.Branches {
branchNames = append(branchNames, branch.Name)
-
span.SetAttributes(attribute.Int("branches.count", len(branchNames)))
···
defaultBranchResp, err := us.DefaultBranch(f.OwnerDid(), f.RepoName)
log.Println("failed to reach knotserver", err)
-
span.SetAttributes(attribute.String("error", "failed_to_reach_knotserver_for_default_branch"))
defaultBranch = defaultBranchResp.Branch
-
span.SetAttributes(attribute.String("default_branch", defaultBranch))
s.pages.RepoSettings(w, pages.RepoSettingsParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
Collaborators: repoCollaborators,
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
···
return collaborators, nil
-
func (f *FullyResolvedRepo) RepoInfo(ctx context.Context, s *State, u *auth.User) repoinfo.RepoInfo {
-
ctx, span := s.t.TraceStart(ctx, "RepoInfo")
isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))
-
span.SetAttributes(attribute.Bool("is_starred", isStarred))
starCount, err := db.GetStarCount(s.db, f.RepoAt)
log.Println("failed to get star count for ", f.RepoAt)
issueCount, err := db.GetIssueCount(s.db, f.RepoAt)
log.Println("failed to get issue count for ", f.RepoAt)
pullCount, err := db.GetPullCount(s.db, f.RepoAt)
log.Println("failed to get issue count for ", f.RepoAt)
-
attribute.Int("stats.stars", starCount),
-
attribute.Int("stats.issues.open", issueCount.Open),
-
attribute.Int("stats.issues.closed", issueCount.Closed),
-
attribute.Int("stats.pulls.open", pullCount.Open),
-
attribute.Int("stats.pulls.closed", pullCount.Closed),
-
attribute.Int("stats.pulls.merged", pullCount.Merged),
-
source, err := db.GetRepoSource(ctx, s.db, f.RepoAt)
if errors.Is(err, sql.ErrNoRows) {
log.Println("failed to get repo source for ", f.RepoAt, err)
-
span.SetAttributes(attribute.String("source", source))
-
sourceRepo, err = db.GetRepoByAtUri(ctx, s.db, source)
log.Println("failed to get repo by at uri", err)
var sourceHandle *identity.Identity
-
sourceHandle, err = s.resolver.ResolveIdent(ctx, sourceRepo.Did)
log.Println("failed to resolve source repo", err)
-
} else if sourceHandle != nil {
-
span.SetAttributes(attribute.String("source_handle", sourceHandle.Handle.String()))
-
span.SetAttributes(attribute.String("knot", knot))
us, err := NewUnsignedClient(knot, s.config.Dev)
log.Printf("failed to create unsigned client for %s: %v", knot, err)
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err)
body, err := io.ReadAll(resp.Body)
log.Printf("error reading branch response body: %v", err)
var branchesResp types.RepoBranchesResponse
if err := json.Unmarshal(body, &branchesResp); err != nil {
log.Printf("error parsing branch response: %v", err)
···
if len(branchesResp.Branches) == 0 {
-
attribute.Int("branches.count", len(branchesResp.Branches)),
-
attribute.Bool("disable_fork", disableFork),
···
func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoSingleIssue")
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
-
span.SetAttributes(attribute.Int("issue_id", issueIdInt))
-
issue, comments, err := db.GetIssueWithComments(ctx, s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue and comments", err)
-
span.SetStatus(codes.Error, "failed to get issue and comments")
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
-
attribute.Int("comments.count", len(comments)),
-
attribute.String("issue.title", issue.Title),
-
attribute.String("issue.owner_did", issue.OwnerDid),
-
issueOwnerIdent, err := s.resolver.ResolveIdent(ctx, issue.OwnerDid)
log.Println("failed to resolve issue owner", err)
-
span.SetStatus(codes.Error, "failed to resolve issue owner")
identsToResolve := make([]string, len(comments))
for i, comment := range comments {
identsToResolve[i] = comment.OwnerDid
-
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
IssueOwnerHandle: issueOwnerIdent.Handle.String(),
DidHandleMap: didHandleMap,
func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "CloseIssue")
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
-
span.SetAttributes(attribute.Int("issue_id", issueIdInt))
-
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue", err)
-
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
-
collaborators, err := f.Collaborators(ctx, s)
log.Println("failed to fetch repo collaborators: %w", err)
-
span.SetStatus(codes.Error, "failed to fetch repo collaborators")
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
return user.Did == collab.Did
isIssueOwner := user.Did == issue.OwnerDid
-
attribute.Bool("is_collaborator", isCollaborator),
-
attribute.Bool("is_issue_owner", isIssueOwner),
// TODO: make this more granular
if isIssueOwner || isCollaborator {
closed := tangled.RepoIssueStateClosed
client, _ := s.auth.AuthorizedClient(r)
-
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueStateNSID,
···
log.Println("failed to update issue state", err)
-
span.SetStatus(codes.Error, "failed to update issue state in PDS")
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
···
err := db.CloseIssue(s.db, f.RepoAt, issueIdInt)
log.Println("failed to close issue", err)
-
span.SetStatus(codes.Error, "failed to close issue in database")
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
···
log.Println("user is not permitted to close issue")
-
span.SetAttributes(attribute.Bool("permission_denied", true))
http.Error(w, "for biden", http.StatusUnauthorized)
func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "ReopenIssue")
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
-
span.SetAttributes(attribute.Int("issue_id", issueIdInt))
-
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue", err)
-
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
-
collaborators, err := f.Collaborators(ctx, s)
log.Println("failed to fetch repo collaborators: %w", err)
-
span.SetStatus(codes.Error, "failed to fetch repo collaborators")
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
return user.Did == collab.Did
isIssueOwner := user.Did == issue.OwnerDid
-
attribute.Bool("is_collaborator", isCollaborator),
-
attribute.Bool("is_issue_owner", isIssueOwner),
if isCollaborator || isIssueOwner {
err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt)
log.Println("failed to reopen issue", err)
-
span.SetStatus(codes.Error, "failed to reopen issue")
s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
···
log.Println("user is not the owner of the repo")
-
span.SetAttributes(attribute.Bool("permission_denied", true))
http.Error(w, "forbidden", http.StatusUnauthorized)
func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "NewIssueComment")
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
-
attribute.Int("issue_id", issueIdInt),
-
attribute.String("method", r.Method),
body := r.FormValue("body")
-
span.SetAttributes(attribute.Bool("missing_body", true))
s.pages.Notice(w, "issue", "Body is required")
···
commentId := mathrand.IntN(1000000)
-
attribute.Int("comment_id", commentId),
-
attribute.String("rkey", rkey),
err := db.NewIssueComment(s.db, &db.Comment{
···
log.Println("failed to create comment", err)
-
span.SetStatus(codes.Error, "failed to create comment in database")
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
···
issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue at", err)
-
span.SetStatus(codes.Error, "failed to get issue at")
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
-
span.SetAttributes(attribute.String("issue_at", issueAt))
atUri := f.RepoAt.String()
client, _ := s.auth.AuthorizedClient(r)
-
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueCommentNSID,
···
log.Println("failed to create comment", err)
-
span.SetStatus(codes.Error, "failed to create comment in PDS")
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
···
func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "IssueComment")
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
···
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.SetStatus(codes.Error, "failed to parse comment id")
-
attribute.Int("issue_id", issueIdInt),
-
attribute.Int("comment_id", commentIdInt),
-
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue", err)
-
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
···
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
http.Error(w, "bad comment id", http.StatusBadRequest)
-
span.SetStatus(codes.Error, "failed to get comment")
-
identity, err := s.resolver.ResolveIdent(ctx, comment.OwnerDid)
log.Println("failed to resolve did")
-
span.SetStatus(codes.Error, "failed to resolve did")
···
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
DidHandleMap: didHandleMap,
···
func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "EditIssueComment")
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
···
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.SetStatus(codes.Error, "failed to parse comment id")
-
attribute.Int("issue_id", issueIdInt),
-
attribute.Int("comment_id", commentIdInt),
-
attribute.String("method", r.Method),
-
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue", err)
-
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
···
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
http.Error(w, "bad comment id", http.StatusBadRequest)
-
span.SetStatus(codes.Error, "failed to get comment")
if comment.OwnerDid != user.Did {
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
-
span.SetAttributes(attribute.Bool("permission_denied", true))
···
s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
···
client, _ := s.auth.AuthorizedClient(r)
-
attribute.String("new_body", newBody),
-
attribute.String("rkey", rkey),
err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
log.Println("failed to perferom update-description query", err)
-
span.SetStatus(codes.Error, "failed to edit comment in database")
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
// rkey is optional, it was introduced later
// update the record on pds
-
ex, err := comatproto.RepoGetRecord(ctx, client, "", tangled.RepoIssueCommentNSID, user.Did, rkey)
-
span.SetStatus(codes.Error, "failed to get record from PDS")
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
···
createdAt := record["createdAt"].(string)
commentIdInt64 := int64(commentIdInt)
-
_, err = comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueCommentNSID,
···
-
span.SetStatus(codes.Error, "failed to put record to PDS")
···
// return new comment body with htmx
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
DidHandleMap: didHandleMap,
func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "DeleteIssueComment")
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.SetStatus(codes.Error, "failed to parse issue id")
-
issue, err := db.GetIssue(ctx, s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue", err)
-
span.SetStatus(codes.Error, "failed to get issue")
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
···
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
-
span.SetStatus(codes.Error, "failed to parse comment id")
-
attribute.Int("issue_id", issueIdInt),
-
attribute.Int("comment_id", commentIdInt),
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
http.Error(w, "bad comment id", http.StatusBadRequest)
-
span.SetStatus(codes.Error, "failed to get comment")
if comment.OwnerDid != user.Did {
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
-
span.SetAttributes(attribute.Bool("permission_denied", true))
if comment.Deleted != nil {
http.Error(w, "comment already deleted", http.StatusBadRequest)
-
span.SetAttributes(attribute.Bool("already_deleted", true))
···
err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
log.Println("failed to delete comment")
-
span.SetStatus(codes.Error, "failed to delete comment in database")
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
client, _ := s.auth.AuthorizedClient(r)
-
_, err = comatproto.RepoDeleteRecord(ctx, client, &comatproto.RepoDeleteRecord_Input{
Collection: tangled.GraphFollowNSID,
-
span.SetStatus(codes.Error, "failed to delete record from PDS")
···
// htmx fragment of comment after deletion
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
DidHandleMap: didHandleMap,
···
func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "RepoIssues")
state := params.Get("state")
···
-
attribute.Bool("is_open", isOpen),
-
attribute.String("state_param", state),
page, ok := r.Context().Value("page").(pagination.Page)
log.Println("failed to get page")
-
span.SetAttributes(attribute.Bool("page_not_found", true))
page = pagination.FirstPage()
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
-
issues, err := db.GetIssues(ctx, s.db, f.RepoAt, isOpen, page)
log.Println("failed to get issues", err)
-
span.SetStatus(codes.Error, "failed to get issues")
s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
-
span.SetAttributes(attribute.Int("issues.count", len(issues)))
identsToResolve := make([]string, len(issues))
for i, issue := range issues {
identsToResolve[i] = issue.OwnerDid
-
resolvedIds := s.resolver.ResolveIdents(ctx, identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
s.pages.RepoIssues(w, pages.RepoIssuesParams{
LoggedInUser: s.auth.GetUser(r),
-
RepoInfo: f.RepoInfo(ctx, s, user),
DidHandleMap: didHandleMap,
···
func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "NewIssue")
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Println("failed to get repo and knot", err)
-
span.SetStatus(codes.Error, "failed to resolve repo")
-
span.SetAttributes(attribute.String("method", r.Method))
s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
title := r.FormValue("title")
body := r.FormValue("body")
-
attribute.String("title", title),
-
attribute.String("body_length", fmt.Sprintf("%d", len(body))),
if title == "" || body == "" {
-
span.SetAttributes(attribute.Bool("form_validation_failed", true))
s.pages.Notice(w, "issues", "Title and body are required")
-
tx, err := s.db.BeginTx(ctx, nil)
-
span.SetStatus(codes.Error, "failed to begin transaction")
s.pages.Notice(w, "issues", "Failed to create issue, try again later")
···
log.Println("failed to create issue", err)
-
span.SetStatus(codes.Error, "failed to create issue in database")
s.pages.Notice(w, "issues", "Failed to create issue.")
···
issueId, err := db.GetIssueId(s.db, f.RepoAt)
log.Println("failed to get issue id", err)
-
span.SetStatus(codes.Error, "failed to get issue id")
s.pages.Notice(w, "issues", "Failed to create issue.")
-
span.SetAttributes(attribute.Int("issue_id", issueId))
client, _ := s.auth.AuthorizedClient(r)
atUri := f.RepoAt.String()
-
span.SetAttributes(attribute.String("rkey", rkey))
-
resp, err := comatproto.RepoPutRecord(ctx, client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueNSID,
Record: &lexutil.LexiconTypeDecoder{
···
log.Println("failed to create issue", err)
-
span.SetStatus(codes.Error, "failed to create issue in PDS")
s.pages.Notice(w, "issues", "Failed to create issue.")
-
span.SetAttributes(attribute.String("issue_uri", resp.Uri))
err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri)
log.Println("failed to set issue at", err)
-
span.SetStatus(codes.Error, "failed to set issue URI in database")
s.pages.Notice(w, "issues", "Failed to create issue.")
···
func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) {
-
ctx, span := s.t.TraceStart(r.Context(), "ForkRepo")
user := s.auth.GetUser(r)
-
f, err := s.fullyResolvedRepo(r.WithContext(ctx))
log.Printf("failed to resolve source repo: %v", err)
-
span.SetStatus(codes.Error, "failed to resolve source repo")
-
attribute.String("method", r.Method),
-
attribute.String("repo_name", f.RepoName),
-
attribute.String("owner_did", f.OwnerDid()),
-
attribute.String("knot", f.Knot),
user := s.auth.GetUser(r)
knots, err := s.enforcer.GetDomainsForUser(user.Did)
-
span.SetStatus(codes.Error, "failed to get domains for user")
s.pages.Notice(w, "repo", "Invalid user account.")
-
span.SetAttributes(attribute.Int("knots.count", len(knots)))
s.pages.ForkRepo(w, pages.ForkRepoParams{
-
RepoInfo: f.RepoInfo(ctx, s, user),
knot := r.FormValue("knot")
-
span.SetAttributes(attribute.Bool("missing_knot", true))
s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
-
span.SetAttributes(attribute.String("target_knot", knot))
ok, err := s.enforcer.E.Enforce(user.Did, knot, knot, "repo:create")
-
attribute.Bool("permission_denied", true),
-
attribute.Bool("enforce_error", err != nil),
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
forkName := fmt.Sprintf("%s", f.RepoName)
-
span.SetAttributes(attribute.String("fork_name", forkName))
// this check is *only* to see if the forked repo name already exists
// in the user's account.
-
existingRepo, err := db.GetRepo(ctx, s.db, user.Did, f.RepoName)
if errors.Is(err, sql.ErrNoRows) {
// no existing repo with this name found, we can use the name as is
-
span.SetAttributes(attribute.Bool("repo_name_available", true))
log.Println("error fetching existing repo from db", err)
-
span.SetStatus(codes.Error, "failed to check for existing repo")
s.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.")
} else if existingRepo != nil {
// repo with this name already exists, append random string
forkName = fmt.Sprintf("%s-%s", forkName, randomString(3))
-
attribute.Bool("repo_name_conflict", true),
-
attribute.String("adjusted_fork_name", forkName),
secret, err := db.GetRegistrationKey(s.db, knot)
-
span.SetStatus(codes.Error, "failed to get registration key")
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot))
client, err := NewSignedClient(knot, secret, s.config.Dev)
-
span.SetStatus(codes.Error, "failed to create signed client")
s.pages.Notice(w, "repo", "Failed to reach knot server.")
···
forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName)
sourceAt := f.RepoAt.String()
-
attribute.String("fork_source_url", forkSourceUrl),
-
attribute.String("source_at", sourceAt),
···
-
span.SetAttributes(attribute.String("rkey", rkey))
-
tx, err := s.db.BeginTx(ctx, nil)
-
span.SetStatus(codes.Error, "failed to begin transaction")
s.pages.Notice(w, "repo", "Failed to save repository information.")
···
err = s.enforcer.E.LoadPolicy()
log.Println("failed to rollback policies")
resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName)
-
span.SetStatus(codes.Error, "failed to fork repo on knot server")
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
-
span.SetAttributes(attribute.Int("fork_response_status", resp.StatusCode))
case http.StatusConflict:
-
span.SetAttributes(attribute.Bool("name_conflict", true))
s.pages.Notice(w, "repo", "A repository with that name already exists.")
case http.StatusInternalServerError:
-
span.SetAttributes(attribute.Bool("server_error", true))
s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
case http.StatusNoContent:
···
xrpcClient, _ := s.auth.AuthorizedClient(r)
createdAt := time.Now().Format(time.RFC3339)
-
atresp, err := comatproto.RepoPutRecord(ctx, xrpcClient, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
···
log.Printf("failed to create record: %s", err)
-
span.SetStatus(codes.Error, "failed to create record in PDS")
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
log.Println("created repo record: ", atresp.Uri)
-
span.SetAttributes(attribute.String("repo_uri", atresp.Uri))
-
err = db.AddRepo(ctx, tx, repo)
-
span.SetStatus(codes.Error, "failed to add repo to database")
s.pages.Notice(w, "repo", "Failed to save repository information.")
···
err = s.enforcer.AddRepo(user.Did, knot, p)
-
span.SetStatus(codes.Error, "failed to set up repository permissions")
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
···
log.Println("failed to commit changes", err)
-
span.SetStatus(codes.Error, "failed to commit transaction")
http.Error(w, err.Error(), http.StatusInternalServerError)
···
err = s.enforcer.E.SavePolicy()
log.Println("failed to update ACLs", err)
-
span.SetStatus(codes.Error, "failed to save policy")
http.Error(w, err.Error(), http.StatusInternalServerError)
···
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/auth"
···
func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
ref := chi.URLParam(r, "ref")
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to fully resolve repo", err)
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
log.Printf("failed to create unsigned client for %s", f.Knot)
···
log.Println("failed to reach knotserver", err)
···
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
···
err = json.Unmarshal(body, &result)
log.Printf("Error unmarshalling response body: %v", err)
···
tagCount := len(result.Tags)
fileCount := len(result.Files)
commitCount, branchCount, tagCount = balanceIndexItems(commitCount, branchCount, tagCount, fileCount)
commitsTrunc := result.Commits[:min(commitCount, len(result.Commits))]
tagsTrunc := result.Tags[:min(tagCount, len(result.Tags))]
···
user := s.auth.GetUser(r)
s.pages.RepoIndexPage(w, pages.RepoIndexParams{
+
RepoInfo: f.RepoInfo(s, user),
RepoIndexResponse: result,
CommitsTrunc: commitsTrunc,
···
func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to fully resolve repo", err)
···
ref := chi.URLParam(r, "ref")
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
log.Println("failed to create unsigned client", err)
resp, err := us.Log(f.OwnerDid(), f.RepoName, ref, page)
log.Println("failed to reach knotserver", err)
body, err := io.ReadAll(resp.Body)
log.Printf("error reading response body: %v", err)
···
err = json.Unmarshal(body, &repolog)
log.Println("failed to parse json response", err)
result, err := us.Tags(f.OwnerDid(), f.RepoName)
log.Println("failed to reach knotserver", err)
···
tagMap[hash] = append(tagMap[hash], tag.Name)
user := s.auth.GetUser(r)
s.pages.RepoLog(w, pages.RepoLogParams{
+
RepoInfo: f.RepoInfo(s, user),
RepoLogResponse: repolog,
EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)),
···
func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
w.WriteHeader(http.StatusBadRequest)
···
user := s.auth.GetUser(r)
s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
+
RepoInfo: f.RepoInfo(s, user),
func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
w.WriteHeader(http.StatusBadRequest)
···
rkey := repoAt.RecordKey().String()
log.Println("invalid aturi for repo", err)
w.WriteHeader(http.StatusInternalServerError)
user := s.auth.GetUser(r)
s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
+
RepoInfo: f.RepoInfo(s, user),
user := s.auth.GetUser(r)
newDescription := r.FormValue("description")
client, _ := s.auth.AuthorizedClient(r)
+
err = db.UpdateDescription(s.db, string(repoAt), newDescription)
+
log.Println("failed to perferom update-description query", err)
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
// this is a bit of a pain because the golang atproto impl does not allow nil SwapRecord field
// SwapRecord is optional and should happen automagically, but given that it does not, we have to perform two requests
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, user.Did, rkey)
s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
···
+
log.Println("failed to perferom update-description query", err)
s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.")
+
newRepoInfo := f.RepoInfo(s, user)
newRepoInfo.Description = newDescription
s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
···
func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to fully resolve repo", err)
ref := chi.URLParam(r, "ref")
···
if !plumbing.IsHash(ref) {
+
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/commit/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref))
log.Println("failed to reach knotserver", err)
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
···
err = json.Unmarshal(body, &result)
log.Println("failed to parse response:", err)
user := s.auth.GetUser(r)
s.pages.RepoCommit(w, pages.RepoCommitParams{
+
RepoInfo: f.RepoInfo(s, user),
RepoCommitResponse: result,
EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}),
···
func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to fully resolve repo", err)
···
+
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
log.Println("failed to reach knotserver", err)
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
···
err = json.Unmarshal(body, &result)
log.Println("failed to parse response:", err)
// 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 {
+
http.Redirect(w, r, fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), ref, result.Parent), http.StatusFound)
···
BreadCrumbs: breadcrumbs,
BaseTreeLink: baseTreeLink,
BaseBlobLink: baseBlobLink,
+
RepoInfo: f.RepoInfo(s, user),
RepoTreeResponse: result,
func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
log.Println("failed to create unsigned client", err)
result, err := us.Tags(f.OwnerDid(), f.RepoName)
log.Println("failed to reach knotserver", err)
artifacts, err := db.GetArtifact(s.db, db.Filter("repo_at", f.RepoAt))
log.Println("failed grab artifacts", err)
// convert artifacts to map for easy UI building
artifactMap := make(map[plumbing.Hash][]db.Artifact)
···
user := s.auth.GetUser(r)
s.pages.RepoTags(w, pages.RepoTagsParams{
+
RepoInfo: f.RepoInfo(s, user),
RepoTagsResponse: *result,
ArtifactMap: artifactMap,
DanglingArtifacts: danglingArtifacts,
···
func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
log.Println("failed to create unsigned client", err)
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
log.Println("failed to reach knotserver", err)
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
···
err = json.Unmarshal(body, &result)
log.Println("failed to parse response:", err)
slices.SortFunc(result.Branches, func(a, b types.Branch) int {
···
user := s.auth.GetUser(r)
s.pages.RepoBranches(w, pages.RepoBranchesParams{
+
RepoInfo: f.RepoInfo(s, user),
RepoBranchesResponse: result,
func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
···
+
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
log.Println("failed to reach knotserver", err)
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
···
err = json.Unmarshal(body, &result)
log.Println("failed to parse response:", err)
···
showRendered = r.URL.Query().Get("code") != "true"
user := s.auth.GetUser(r)
s.pages.RepoBlob(w, pages.RepoBlobParams{
+
RepoInfo: f.RepoInfo(s, user),
RepoBlobResponse: result,
BreadCrumbs: breadcrumbs,
ShowRendered: showRendered,
···
func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
···
+
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
log.Println("failed to reach knotserver", err)
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
···
err = json.Unmarshal(body, &result)
log.Println("failed to parse response:", err)
w.Header().Set("Content-Type", "application/octet-stream")
···
func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
collaborator := r.FormValue("collaborator")
http.Error(w, "malformed form", http.StatusBadRequest)
+
collaboratorIdent, err := s.resolver.ResolveIdent(r.Context(), collaborator)
w.Write([]byte("failed to resolve collaborator did to a handle"))
log.Printf("adding %s to %s\n", collaboratorIdent.Handle.String(), f.Knot)
// TODO: create an atproto record for this
secret, err := db.GetRegistrationKey(s.db, f.Knot)
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
log.Println("failed to create client to ", f.Knot)
ksResp, err := ksClient.AddCollaborator(f.OwnerDid(), f.RepoName, collaboratorIdent.DID.String())
log.Printf("failed to make request to %s: %s", f.Knot, err)
if ksResp.StatusCode != http.StatusNoContent {
w.Write([]byte(fmt.Sprint("knotserver failed to add collaborator: ", err)))
+
tx, err := s.db.BeginTx(r.Context(), nil)
log.Println("failed to start tx")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
···
err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
+
err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
···
log.Println("failed to commit changes", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
···
err = s.enforcer.E.SavePolicy()
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
w.Write([]byte(fmt.Sprint("added collaborator: ", collaboratorIdent.Handle.String())))
func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
// remove record from pds
xrpcClient, _ := s.auth.AuthorizedClient(r)
repoRkey := f.RepoAt.RecordKey().String()
+
_, err = comatproto.RepoDeleteRecord(r.Context(), xrpcClient, &comatproto.RepoDeleteRecord_Input{
Collection: tangled.RepoNSID,
log.Printf("failed to delete record: %s", err)
s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.")
log.Println("removed repo record ", f.RepoAt.String())
secret, err := db.GetRegistrationKey(s.db, f.Knot)
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
log.Println("failed to create client to ", f.Knot)
ksResp, err := ksClient.RemoveRepo(f.OwnerDid(), f.RepoName)
log.Printf("failed to make request to %s: %s", f.Knot, err)
if ksResp.StatusCode != http.StatusNoContent {
log.Println("failed to remove repo from knot, continuing anyway ", f.Knot)
log.Println("removed repo from knot ", f.Knot)
+
tx, err := s.db.BeginTx(r.Context(), nil)
log.Println("failed to start tx")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
···
err = s.enforcer.E.LoadPolicy()
log.Println("failed to rollback policies")
// remove collaborator RBAC
repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
s.pages.Notice(w, "settings-delete", "Failed to remove collaborators")
for _, c := range repoCollaborators {
s.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
···
err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules")
+
err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName)
s.pages.Notice(w, "settings-delete", "Failed to update appview")
···
log.Println("failed to commit changes", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
···
err = s.enforcer.E.SavePolicy()
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
···
func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
branch := r.FormValue("branch")
http.Error(w, "malformed form", http.StatusBadRequest)
secret, err := db.GetRegistrationKey(s.db, f.Knot)
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
log.Println("failed to create client to ", f.Knot)
ksResp, err := ksClient.SetDefaultBranch(f.OwnerDid(), f.RepoName, branch)
log.Printf("failed to make request to %s: %s", f.Knot, err)
if ksResp.StatusCode != http.StatusNoContent {
s.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.")
···
func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
// for now, this is just pubkeys
user := s.auth.GetUser(r)
+
repoCollaborators, err := f.Collaborators(r.Context(), s)
log.Println("failed to get collaborators", err)
isCollaboratorInviteAllowed := false
···
isCollaboratorInviteAllowed = true
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
log.Println("failed to create unsigned client", err)
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
log.Println("failed to reach knotserver", err)
body, err := io.ReadAll(resp.Body)
log.Printf("Error reading response body: %v", err)
var result types.RepoBranchesResponse
err = json.Unmarshal(body, &result)
log.Println("failed to parse response:", err)
for _, branch := range result.Branches {
branchNames = append(branchNames, branch.Name)
···
defaultBranchResp, err := us.DefaultBranch(f.OwnerDid(), f.RepoName)
log.Println("failed to reach knotserver", err)
defaultBranch = defaultBranchResp.Branch
s.pages.RepoSettings(w, pages.RepoSettingsParams{
+
RepoInfo: f.RepoInfo(s, user),
Collaborators: repoCollaborators,
IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed,
···
return collaborators, nil
+
func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) repoinfo.RepoInfo {
isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt))
starCount, err := db.GetStarCount(s.db, f.RepoAt)
log.Println("failed to get star count for ", f.RepoAt)
issueCount, err := db.GetIssueCount(s.db, f.RepoAt)
log.Println("failed to get issue count for ", f.RepoAt)
pullCount, err := db.GetPullCount(s.db, f.RepoAt)
log.Println("failed to get issue count for ", f.RepoAt)
+
source, err := db.GetRepoSource(s.db, f.RepoAt)
if errors.Is(err, sql.ErrNoRows) {
log.Println("failed to get repo source for ", f.RepoAt, err)
+
sourceRepo, err = db.GetRepoByAtUri(s.db, source)
log.Println("failed to get repo by at uri", err)
var sourceHandle *identity.Identity
+
sourceHandle, err = s.resolver.ResolveIdent(context.Background(), sourceRepo.Did)
log.Println("failed to resolve source repo", err)
us, err := NewUnsignedClient(knot, s.config.Dev)
log.Printf("failed to create unsigned client for %s: %v", knot, err)
resp, err := us.Branches(f.OwnerDid(), f.RepoName)
log.Printf("failed to get branches for %s/%s: %v", f.OwnerDid(), f.RepoName, err)
body, err := io.ReadAll(resp.Body)
log.Printf("error reading branch response body: %v", err)
var branchesResp types.RepoBranchesResponse
if err := json.Unmarshal(body, &branchesResp); err != nil {
log.Printf("error parsing branch response: %v", err)
···
if len(branchesResp.Branches) == 0 {
···
func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue and comments", err)
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
+
issueOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), issue.OwnerDid)
log.Println("failed to resolve issue owner", err)
identsToResolve := make([]string, len(comments))
for i, comment := range comments {
identsToResolve[i] = comment.OwnerDid
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
+
RepoInfo: f.RepoInfo(s, user),
IssueOwnerHandle: issueOwnerIdent.Handle.String(),
DidHandleMap: didHandleMap,
func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue", err)
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
+
collaborators, err := f.Collaborators(r.Context(), s)
log.Println("failed to fetch repo collaborators: %w", err)
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
return user.Did == collab.Did
isIssueOwner := user.Did == issue.OwnerDid
// TODO: make this more granular
if isIssueOwner || isCollaborator {
closed := tangled.RepoIssueStateClosed
client, _ := s.auth.AuthorizedClient(r)
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueStateNSID,
···
log.Println("failed to update issue state", err)
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
···
err := db.CloseIssue(s.db, f.RepoAt, issueIdInt)
log.Println("failed to close issue", err)
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
···
log.Println("user is not permitted to close issue")
http.Error(w, "for biden", http.StatusUnauthorized)
func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue", err)
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
+
collaborators, err := f.Collaborators(r.Context(), s)
log.Println("failed to fetch repo collaborators: %w", err)
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
return user.Did == collab.Did
isIssueOwner := user.Did == issue.OwnerDid
if isCollaborator || isIssueOwner {
err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt)
log.Println("failed to reopen issue", err)
s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
···
log.Println("user is not the owner of the repo")
http.Error(w, "forbidden", http.StatusUnauthorized)
func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
body := r.FormValue("body")
s.pages.Notice(w, "issue", "Body is required")
···
commentId := mathrand.IntN(1000000)
err := db.NewIssueComment(s.db, &db.Comment{
···
log.Println("failed to create comment", err)
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
···
issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue at", err)
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
atUri := f.RepoAt.String()
client, _ := s.auth.AuthorizedClient(r)
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueCommentNSID,
···
log.Println("failed to create comment", err)
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
···
func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
···
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue", err)
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
···
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
http.Error(w, "bad comment id", http.StatusBadRequest)
+
identity, err := s.resolver.ResolveIdent(r.Context(), comment.OwnerDid)
log.Println("failed to resolve did")
···
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
+
RepoInfo: f.RepoInfo(s, user),
DidHandleMap: didHandleMap,
···
func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
···
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue", err)
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
···
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
http.Error(w, "bad comment id", http.StatusBadRequest)
if comment.OwnerDid != user.Did {
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
···
s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
+
RepoInfo: f.RepoInfo(s, user),
···
client, _ := s.auth.AuthorizedClient(r)
err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
log.Println("failed to perferom update-description query", err)
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
// rkey is optional, it was introduced later
// update the record on pds
+
ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoIssueCommentNSID, user.Did, rkey)
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
···
createdAt := record["createdAt"].(string)
commentIdInt64 := int64(commentIdInt)
+
_, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueCommentNSID,
···
···
// return new comment body with htmx
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
+
RepoInfo: f.RepoInfo(s, user),
DidHandleMap: didHandleMap,
func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
···
http.Error(w, "bad issue id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
+
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
log.Println("failed to get issue", err)
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
···
http.Error(w, "bad comment id", http.StatusBadRequest)
log.Println("failed to parse issue id", err)
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
http.Error(w, "bad comment id", http.StatusBadRequest)
if comment.OwnerDid != user.Did {
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
if comment.Deleted != nil {
http.Error(w, "comment already deleted", http.StatusBadRequest)
···
err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
log.Println("failed to delete comment")
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
client, _ := s.auth.AuthorizedClient(r)
+
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
Collection: tangled.GraphFollowNSID,
···
// htmx fragment of comment after deletion
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
+
RepoInfo: f.RepoInfo(s, user),
DidHandleMap: didHandleMap,
···
func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
state := params.Get("state")
···
page, ok := r.Context().Value("page").(pagination.Page)
log.Println("failed to get page")
page = pagination.FirstPage()
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
+
issues, err := db.GetIssues(s.db, f.RepoAt, isOpen, page)
log.Println("failed to get issues", err)
s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
identsToResolve := make([]string, len(issues))
for i, issue := range issues {
identsToResolve[i] = issue.OwnerDid
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
s.pages.RepoIssues(w, pages.RepoIssuesParams{
LoggedInUser: s.auth.GetUser(r),
+
RepoInfo: f.RepoInfo(s, user),
DidHandleMap: didHandleMap,
···
func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
log.Println("failed to get repo and knot", err)
s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
+
RepoInfo: f.RepoInfo(s, user),
title := r.FormValue("title")
body := r.FormValue("body")
if title == "" || body == "" {
s.pages.Notice(w, "issues", "Title and body are required")
+
tx, err := s.db.BeginTx(r.Context(), nil)
s.pages.Notice(w, "issues", "Failed to create issue, try again later")
···
log.Println("failed to create issue", err)
s.pages.Notice(w, "issues", "Failed to create issue.")
···
issueId, err := db.GetIssueId(s.db, f.RepoAt)
log.Println("failed to get issue id", err)
s.pages.Notice(w, "issues", "Failed to create issue.")
client, _ := s.auth.AuthorizedClient(r)
atUri := f.RepoAt.String()
+
resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoIssueNSID,
Record: &lexutil.LexiconTypeDecoder{
···
log.Println("failed to create issue", err)
s.pages.Notice(w, "issues", "Failed to create issue.")
err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri)
log.Println("failed to set issue at", err)
s.pages.Notice(w, "issues", "Failed to create issue.")
···
func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
f, err := s.fullyResolvedRepo(r)
log.Printf("failed to resolve source repo: %v", err)
user := s.auth.GetUser(r)
knots, err := s.enforcer.GetDomainsForUser(user.Did)
s.pages.Notice(w, "repo", "Invalid user account.")
s.pages.ForkRepo(w, pages.ForkRepoParams{
+
RepoInfo: f.RepoInfo(s, user),
knot := r.FormValue("knot")
s.pages.Notice(w, "repo", "Invalid form submission—missing knot domain.")
ok, err := s.enforcer.E.Enforce(user.Did, knot, knot, "repo:create")
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
forkName := fmt.Sprintf("%s", f.RepoName)
// this check is *only* to see if the forked repo name already exists
// in the user's account.
+
existingRepo, err := db.GetRepo(s.db, user.Did, f.RepoName)
if errors.Is(err, sql.ErrNoRows) {
// no existing repo with this name found, we can use the name as is
log.Println("error fetching existing repo from db", err)
s.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.")
} else if existingRepo != nil {
// repo with this name already exists, append random string
forkName = fmt.Sprintf("%s-%s", forkName, randomString(3))
secret, err := db.GetRegistrationKey(s.db, knot)
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot))
client, err := NewSignedClient(knot, secret, s.config.Dev)
s.pages.Notice(w, "repo", "Failed to reach knot server.")
···
forkSourceUrl := fmt.Sprintf("%s://%s/%s/%s", uri, f.Knot, f.OwnerDid(), f.RepoName)
sourceAt := f.RepoAt.String()
···
+
tx, err := s.db.BeginTx(r.Context(), nil)
s.pages.Notice(w, "repo", "Failed to save repository information.")
···
err = s.enforcer.E.LoadPolicy()
log.Println("failed to rollback policies")
resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName)
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
case http.StatusConflict:
s.pages.Notice(w, "repo", "A repository with that name already exists.")
case http.StatusInternalServerError:
s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
case http.StatusNoContent:
···
xrpcClient, _ := s.auth.AuthorizedClient(r)
createdAt := time.Now().Format(time.RFC3339)
+
atresp, err := comatproto.RepoPutRecord(r.Context(), xrpcClient, &comatproto.RepoPutRecord_Input{
Collection: tangled.RepoNSID,
···
log.Printf("failed to create record: %s", err)
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
log.Println("created repo record: ", atresp.Uri)
+
err = db.AddRepo(tx, repo)
s.pages.Notice(w, "repo", "Failed to save repository information.")
···
err = s.enforcer.AddRepo(user.Did, knot, p)
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
···
log.Println("failed to commit changes", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
···
err = s.enforcer.E.SavePolicy()
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)