forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

appview: repo: refactor into its own package

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.sh>

Changed files
+460 -390
appview
+16 -2
appview/pulls/pulls.go
···
posthog posthog.Client
}
-
func New(oauth *oauth.OAuth, repoResolver *reporesolver.RepoResolver, pages *pages.Pages, resolver *idresolver.Resolver, db *db.DB, config *appview.Config) *Pulls {
-
return &Pulls{oauth: oauth, repoResolver: repoResolver, pages: pages, idResolver: resolver, db: db, config: config}
}
// htmx fragment
···
posthog posthog.Client
}
+
func New(
+
oauth *oauth.OAuth,
+
repoResolver *reporesolver.RepoResolver,
+
pages *pages.Pages,
+
resolver *idresolver.Resolver,
+
db *db.DB,
+
config *appview.Config,
+
) *Pulls {
+
return &Pulls{
+
oauth: oauth,
+
repoResolver: repoResolver,
+
pages: pages,
+
idResolver: resolver,
+
db: db,
+
config: config,
+
}
}
// htmx fragment
+100
appview/repo/router.go
···
···
+
package repo
+
+
import (
+
"net/http"
+
+
"github.com/go-chi/chi/v5"
+
"tangled.sh/tangled.sh/core/appview/middleware"
+
)
+
+
func (rp *Repo) Router(mw *middleware.Middleware) http.Handler {
+
r := chi.NewRouter()
+
r.Get("/", rp.RepoIndex)
+
r.Get("/commits/{ref}", rp.RepoLog)
+
r.Route("/tree/{ref}", func(r chi.Router) {
+
r.Get("/", rp.RepoIndex)
+
r.Get("/*", rp.RepoTree)
+
})
+
r.Get("/commit/{ref}", rp.RepoCommit)
+
r.Get("/branches", rp.RepoBranches)
+
r.Route("/tags", func(r chi.Router) {
+
r.Get("/", rp.RepoTags)
+
r.Route("/{tag}", func(r chi.Router) {
+
r.Use(middleware.AuthMiddleware(rp.oauth))
+
// require auth to download for now
+
r.Get("/download/{file}", rp.DownloadArtifact)
+
+
// require repo:push to upload or delete artifacts
+
//
+
// additionally: only the uploader can truly delete an artifact
+
// (record+blob will live on their pds)
+
r.Group(func(r chi.Router) {
+
r.With(mw.RepoPermissionMiddleware("repo:push"))
+
r.Post("/upload", rp.AttachArtifact)
+
r.Delete("/{file}", rp.DeleteArtifact)
+
})
+
})
+
})
+
r.Get("/blob/{ref}/*", rp.RepoBlob)
+
r.Get("/raw/{ref}/*", rp.RepoBlobRaw)
+
+
r.Route("/issues", func(r chi.Router) {
+
r.With(middleware.Paginate).Get("/", rp.RepoIssues)
+
r.Get("/{issue}", rp.RepoSingleIssue)
+
+
r.Group(func(r chi.Router) {
+
r.Use(middleware.AuthMiddleware(rp.oauth))
+
r.Get("/new", rp.NewIssue)
+
r.Post("/new", rp.NewIssue)
+
r.Post("/{issue}/comment", rp.NewIssueComment)
+
r.Route("/{issue}/comment/{comment_id}/", func(r chi.Router) {
+
r.Get("/", rp.IssueComment)
+
r.Delete("/", rp.DeleteIssueComment)
+
r.Get("/edit", rp.EditIssueComment)
+
r.Post("/edit", rp.EditIssueComment)
+
})
+
r.Post("/{issue}/close", rp.CloseIssue)
+
r.Post("/{issue}/reopen", rp.ReopenIssue)
+
})
+
})
+
+
r.Route("/fork", func(r chi.Router) {
+
r.Use(middleware.AuthMiddleware(rp.oauth))
+
r.Get("/", rp.ForkRepo)
+
r.Post("/", rp.ForkRepo)
+
r.With(mw.RepoPermissionMiddleware("repo:owner")).Route("/sync", func(r chi.Router) {
+
r.Post("/", rp.SyncRepoFork)
+
})
+
})
+
+
r.Route("/compare", func(r chi.Router) {
+
r.Get("/", rp.RepoCompareNew) // start an new comparison
+
+
// we have to wildcard here since we want to support GitHub's compare syntax
+
// /compare/{ref1}...{ref2}
+
// for example:
+
// /compare/master...some/feature
+
// /compare/master...example.com:another/feature <- this is a fork
+
r.Get("/{base}/{head}", rp.RepoCompare)
+
r.Get("/*", rp.RepoCompare)
+
})
+
+
// settings routes, needs auth
+
r.Group(func(r chi.Router) {
+
r.Use(middleware.AuthMiddleware(rp.oauth))
+
// repo description can only be edited by owner
+
r.With(mw.RepoPermissionMiddleware("repo:owner")).Route("/description", func(r chi.Router) {
+
r.Put("/", rp.RepoDescription)
+
r.Get("/", rp.RepoDescription)
+
r.Get("/edit", rp.RepoDescriptionEdit)
+
})
+
r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) {
+
r.Get("/", rp.RepoSettings)
+
r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", rp.AddCollaborator)
+
r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", rp.DeleteRepo)
+
r.Put("/branches/default", rp.SetDefaultBranch)
+
})
+
})
+
+
return r
+
}
+38 -38
appview/state/artifact.go appview/repo/artifact.go
···
-
package state
import (
"fmt"
···
)
// TODO: proper statuses here on early exit
-
func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
tagParam := chi.URLParam(r, "tag")
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
-
s.pages.Notice(w, "upload", "failed to upload artifact, error in repo resolution")
return
}
-
tag, err := s.resolveTag(f, tagParam)
if err != nil {
log.Println("failed to resolve tag", err)
-
s.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
return
}
file, handler, err := r.FormFile("artifact")
if err != nil {
log.Println("failed to upload artifact", err)
-
s.pages.Notice(w, "upload", "failed to upload artifact")
return
}
defer file.Close()
-
client, err := s.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
-
s.pages.Notice(w, "upload", "failed to get authorized client")
return
}
uploadBlobResp, err := client.RepoUploadBlob(r.Context(), file)
if err != nil {
log.Println("failed to upload blob", err)
-
s.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.")
return
}
···
})
if err != nil {
log.Println("failed to create record", err)
-
s.pages.Notice(w, "upload", "Failed to create artifact record. Try again later.")
return
}
log.Println(putRecordResp.Uri)
-
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
-
s.pages.Notice(w, "upload", "Failed to create artifact. Try again later.")
return
}
defer tx.Rollback()
···
err = db.AddArtifact(tx, artifact)
if err != nil {
log.Println("failed to add artifact record to db", err)
-
s.pages.Notice(w, "upload", "Failed to create artifact. Try again later.")
return
}
err = tx.Commit()
if err != nil {
log.Println("failed to add artifact record to db")
-
s.pages.Notice(w, "upload", "Failed to create artifact. Try again later.")
return
}
-
s.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
Artifact: artifact,
···
}
// TODO: proper statuses here on early exit
-
func (s *State) DownloadArtifact(w http.ResponseWriter, r *http.Request) {
tagParam := chi.URLParam(r, "tag")
filename := chi.URLParam(r, "file")
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
tag, err := s.resolveTag(f, tagParam)
if err != nil {
log.Println("failed to resolve tag", err)
-
s.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
return
}
-
client, err := s.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
return
}
artifacts, err := db.GetArtifact(
-
s.db,
db.FilterEq("repo_at", f.RepoAt),
db.FilterEq("tag", tag.Tag.Hash[:]),
db.FilterEq("name", filename),
···
}
// TODO: proper statuses here on early exit
-
func (s *State) DeleteArtifact(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
tagParam := chi.URLParam(r, "tag")
filename := chi.URLParam(r, "file")
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
client, _ := s.oauth.AuthorizedClient(r)
tag := plumbing.NewHash(tagParam)
artifacts, err := db.GetArtifact(
-
s.db,
db.FilterEq("repo_at", f.RepoAt),
db.FilterEq("tag", tag[:]),
db.FilterEq("name", filename),
)
if err != nil {
log.Println("failed to get artifacts", err)
-
s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
return
}
if len(artifacts) != 1 {
-
s.pages.Notice(w, "remove", "Unable to find artifact.")
return
}
···
if user.Did != artifact.Did {
log.Println("user not authorized to delete artifact", err)
-
s.pages.Notice(w, "remove", "Unauthorized deletion of artifact.")
return
}
···
})
if err != nil {
log.Println("failed to get blob from pds", err)
-
s.pages.Notice(w, "remove", "Failed to remove blob from PDS.")
return
}
-
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
-
s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
return
}
defer tx.Rollback()
···
)
if err != nil {
log.Println("failed to remove artifact record from db", err)
-
s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
return
}
err = tx.Commit()
if err != nil {
log.Println("failed to remove artifact record from db")
-
s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
return
}
w.Write([]byte{})
}
-
func (s *State) resolveTag(f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) {
tagParam, err := url.QueryUnescape(tagParam)
if err != nil {
return nil, err
}
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
if err != nil {
return nil, err
}
···
+
package repo
import (
"fmt"
···
)
// TODO: proper statuses here on early exit
+
func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
tagParam := chi.URLParam(r, "tag")
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
+
rp.pages.Notice(w, "upload", "failed to upload artifact, error in repo resolution")
return
}
+
tag, err := rp.resolveTag(f, tagParam)
if err != nil {
log.Println("failed to resolve tag", err)
+
rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
return
}
file, handler, err := r.FormFile("artifact")
if err != nil {
log.Println("failed to upload artifact", err)
+
rp.pages.Notice(w, "upload", "failed to upload artifact")
return
}
defer file.Close()
+
client, err := rp.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
+
rp.pages.Notice(w, "upload", "failed to get authorized client")
return
}
uploadBlobResp, err := client.RepoUploadBlob(r.Context(), file)
if err != nil {
log.Println("failed to upload blob", err)
+
rp.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.")
return
}
···
})
if err != nil {
log.Println("failed to create record", err)
+
rp.pages.Notice(w, "upload", "Failed to create artifact record. Try again later.")
return
}
log.Println(putRecordResp.Uri)
+
tx, err := rp.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
+
rp.pages.Notice(w, "upload", "Failed to create artifact. Try again later.")
return
}
defer tx.Rollback()
···
err = db.AddArtifact(tx, artifact)
if err != nil {
log.Println("failed to add artifact record to db", err)
+
rp.pages.Notice(w, "upload", "Failed to create artifact. Try again later.")
return
}
err = tx.Commit()
if err != nil {
log.Println("failed to add artifact record to db")
+
rp.pages.Notice(w, "upload", "Failed to create artifact. Try again later.")
return
}
+
rp.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
Artifact: artifact,
···
}
// TODO: proper statuses here on early exit
+
func (rp *Repo) DownloadArtifact(w http.ResponseWriter, r *http.Request) {
tagParam := chi.URLParam(r, "tag")
filename := chi.URLParam(r, "file")
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
tag, err := rp.resolveTag(f, tagParam)
if err != nil {
log.Println("failed to resolve tag", err)
+
rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution")
return
}
+
client, err := rp.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
return
}
artifacts, err := db.GetArtifact(
+
rp.db,
db.FilterEq("repo_at", f.RepoAt),
db.FilterEq("tag", tag.Tag.Hash[:]),
db.FilterEq("name", filename),
···
}
// TODO: proper statuses here on early exit
+
func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
tagParam := chi.URLParam(r, "tag")
filename := chi.URLParam(r, "file")
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
client, _ := rp.oauth.AuthorizedClient(r)
tag := plumbing.NewHash(tagParam)
artifacts, err := db.GetArtifact(
+
rp.db,
db.FilterEq("repo_at", f.RepoAt),
db.FilterEq("tag", tag[:]),
db.FilterEq("name", filename),
)
if err != nil {
log.Println("failed to get artifacts", err)
+
rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
return
}
if len(artifacts) != 1 {
+
rp.pages.Notice(w, "remove", "Unable to find artifact.")
return
}
···
if user.Did != artifact.Did {
log.Println("user not authorized to delete artifact", err)
+
rp.pages.Notice(w, "remove", "Unauthorized deletion of artifact.")
return
}
···
})
if err != nil {
log.Println("failed to get blob from pds", err)
+
rp.pages.Notice(w, "remove", "Failed to remove blob from PDS.")
return
}
+
tx, err := rp.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
+
rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
return
}
defer tx.Rollback()
···
)
if err != nil {
log.Println("failed to remove artifact record from db", err)
+
rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
return
}
err = tx.Commit()
if err != nil {
log.Println("failed to remove artifact record from db")
+
rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.")
return
}
w.Write([]byte{})
}
+
func (rp *Repo) resolveTag(f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) {
tagParam, err := url.QueryUnescape(tagParam)
if err != nil {
return nil, err
}
+
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
if err != nil {
return nil, err
}
+295 -261
appview/state/repo.go appview/repo/repo.go
···
-
package state
import (
"database/sql"
···
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/db"
"tangled.sh/tangled.sh/core/appview/oauth"
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/appview/pages/markup"
···
"tangled.sh/tangled.sh/core/appview/reporesolver"
"tangled.sh/tangled.sh/core/knotclient"
"tangled.sh/tangled.sh/core/patchutil"
"tangled.sh/tangled.sh/core/types"
"github.com/bluesky-social/indigo/atproto/data"
···
lexutil "github.com/bluesky-social/indigo/lex/util"
)
-
func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) {
ref := chi.URLParam(r, "ref")
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
return
}
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
-
s.pages.Error503(w)
return
}
result, err := us.Index(f.OwnerDid(), f.RepoName, ref)
if err != nil {
-
s.pages.Error503(w)
log.Println("failed to reach knotserver", err)
return
}
···
emails := uniqueEmails(commitsTrunc)
-
user := s.oauth.GetUser(r)
repoInfo := f.RepoInfo(user)
-
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("failed to get registration key for %s: %s", f.Knot, err)
-
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
}
-
signedClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
if err != nil {
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
return
···
var forkInfo *types.ForkInfo
if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) {
-
forkInfo, err = getForkInfo(repoInfo, s, f, user, signedClient)
if err != nil {
log.Printf("Failed to fetch fork information: %v", err)
return
···
// non-fatal
}
-
s.pages.RepoIndexPage(w, pages.RepoIndexParams{
LoggedInUser: user,
RepoInfo: repoInfo,
TagMap: tagMap,
···
TagsTrunc: tagsTrunc,
ForkInfo: forkInfo,
BranchesTrunc: branchesTrunc,
-
EmailToDidOrHandle: EmailToDidOrHandle(s, emails),
Languages: repoLanguages,
})
return
···
func getForkInfo(
repoInfo repoinfo.RepoInfo,
-
s *State,
f *reporesolver.ResolvedRepo,
user *oauth.User,
signedClient *knotclient.SignedClient,
···
return &forkInfo, nil
}
-
us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, s.config.Core.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot)
return nil, err
···
return &forkInfo, nil
}
-
func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) {
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
return
···
ref := chi.URLParam(r, "ref")
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
return
···
tagMap[hash] = append(tagMap[hash], tag.Name)
}
-
user := s.oauth.GetUser(r)
-
s.pages.RepoLog(w, pages.RepoLogParams{
LoggedInUser: user,
TagMap: tagMap,
RepoInfo: f.RepoInfo(user),
RepoLogResponse: *repolog,
-
EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)),
})
return
}
-
func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) {
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
w.WriteHeader(http.StatusBadRequest)
return
}
-
user := s.oauth.GetUser(r)
-
s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
RepoInfo: f.RepoInfo(user),
})
return
}
-
func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) {
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
w.WriteHeader(http.StatusBadRequest)
···
return
}
-
user := s.oauth.GetUser(r)
switch r.Method {
case http.MethodGet:
-
s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
RepoInfo: f.RepoInfo(user),
})
return
case http.MethodPut:
-
user := s.oauth.GetUser(r)
newDescription := r.FormValue("description")
-
client, err := s.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get client")
-
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
}
// optimistic update
-
err = db.UpdateDescription(s.db, string(repoAt), newDescription)
if err != nil {
log.Println("failed to perferom update-description query", err)
-
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
}
···
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
if err != nil {
// failed to get record
-
s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
return
}
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
···
if err != nil {
log.Println("failed to perferom update-description query", err)
// failed to get record
-
s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.")
return
}
newRepoInfo := f.RepoInfo(user)
newRepoInfo.Description = newDescription
-
s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
RepoInfo: newRepoInfo,
})
return
}
}
-
func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
return
}
ref := chi.URLParam(r, "ref")
protocol := "http"
-
if !s.config.Core.Dev {
protocol = "https"
}
if !plumbing.IsHash(ref) {
-
s.pages.Error404(w)
return
}
···
return
}
-
user := s.oauth.GetUser(r)
-
s.pages.RepoCommit(w, pages.RepoCommitParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
RepoCommitResponse: result,
-
EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}),
})
return
}
-
func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) {
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
return
···
ref := chi.URLParam(r, "ref")
treePath := chi.URLParam(r, "*")
protocol := "http"
-
if !s.config.Core.Dev {
protocol = "https"
}
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
···
return
}
-
user := s.oauth.GetUser(r)
var breadcrumbs [][]string
breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
···
baseTreeLink := path.Join(f.OwnerSlashRepo(), "tree", ref, treePath)
baseBlobLink := path.Join(f.OwnerSlashRepo(), "blob", ref, treePath)
-
s.pages.RepoTree(w, pages.RepoTreeParams{
LoggedInUser: user,
BreadCrumbs: breadcrumbs,
BaseTreeLink: baseTreeLink,
···
return
}
-
func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) {
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
return
···
return
}
-
artifacts, err := db.GetArtifact(s.db, db.FilterEq("repo_at", f.RepoAt))
if err != nil {
log.Println("failed grab artifacts", err)
return
···
}
}
-
user := s.oauth.GetUser(r)
-
s.pages.RepoTags(w, pages.RepoTagsParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
RepoTagsResponse: *result,
···
return
}
-
func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) {
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
return
···
return strings.Compare(a.Name, b.Name) * -1
})
-
user := s.oauth.GetUser(r)
-
s.pages.RepoBranches(w, pages.RepoBranchesParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
RepoBranchesResponse: *result,
···
return
}
-
func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) {
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
ref := chi.URLParam(r, "ref")
filePath := chi.URLParam(r, "*")
protocol := "http"
-
if !s.config.Core.Dev {
protocol = "https"
}
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
···
showRendered = r.URL.Query().Get("code") != "true"
}
-
user := s.oauth.GetUser(r)
-
s.pages.RepoBlob(w, pages.RepoBlobParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
RepoBlobResponse: result,
···
return
}
-
func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
filePath := chi.URLParam(r, "*")
protocol := "http"
-
if !s.config.Core.Dev {
protocol = "https"
}
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
···
return
}
-
func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) {
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
-
collaboratorIdent, err := s.idResolver.ResolveIdent(r.Context(), collaborator)
if err != nil {
w.Write([]byte("failed to resolve collaborator did to a handle"))
return
···
// TODO: create an atproto record for this
-
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
return
}
-
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
return
···
return
}
-
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
···
}
defer func() {
tx.Rollback()
-
err = s.enforcer.E.LoadPolicy()
if err != nil {
log.Println("failed to rollback policies")
}
}()
-
err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
if err != nil {
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
-
err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
if err != nil {
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
···
return
}
-
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
···
}
-
func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
// remove record from pds
-
xrpcClient, err := s.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
return
···
})
if err != nil {
log.Printf("failed to delete record: %s", err)
-
s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.")
return
}
log.Println("removed repo record ", f.RepoAt.String())
-
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
return
}
-
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
return
···
log.Println("removed repo from knot ", f.Knot)
}
-
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
···
}
defer func() {
tx.Rollback()
-
err = s.enforcer.E.LoadPolicy()
if err != nil {
log.Println("failed to rollback policies")
}
}()
// remove collaborator RBAC
-
repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
if err != nil {
-
s.pages.Notice(w, "settings-delete", "Failed to remove collaborators")
return
}
for _, c := range repoCollaborators {
did := c[0]
-
s.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
}
log.Println("removed collaborators")
// remove repo RBAC
-
err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
if err != nil {
-
s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules")
return
}
// remove repo from db
err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName)
if err != nil {
-
s.pages.Notice(w, "settings-delete", "Failed to update appview")
return
}
log.Println("removed repo from db")
···
return
}
-
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
-
s.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid()))
}
-
func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
-
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
return
}
-
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
return
···
}
if ksResp.StatusCode != http.StatusNoContent {
-
s.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.")
return
}
w.Write([]byte(fmt.Sprint("default branch set to: ", branch)))
}
-
func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) {
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
switch r.Method {
case http.MethodGet:
// for now, this is just pubkeys
-
user := s.oauth.GetUser(r)
repoCollaborators, err := f.Collaborators(r.Context())
if err != nil {
log.Println("failed to get collaborators", err)
···
isCollaboratorInviteAllowed := false
if user != nil {
-
ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())
if err == nil && ok {
isCollaboratorInviteAllowed = true
}
}
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
return
···
return
}
-
s.pages.RepoSettings(w, pages.RepoSettingsParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
Collaborators: repoCollaborators,
···
}
}
-
func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
-
issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue and comments", err)
-
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
-
issueOwnerIdent, err := s.idResolver.ResolveIdent(r.Context(), issue.OwnerDid)
if err != nil {
log.Println("failed to resolve issue owner", err)
}
···
for i, comment := range comments {
identsToResolve[i] = comment.OwnerDid
}
-
resolvedIds := s.idResolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
}
}
-
s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
Issue: *issue,
···
}
-
func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
-
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
-
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
···
closed := tangled.RepoIssueStateClosed
-
client, err := s.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
return
···
if err != nil {
log.Println("failed to update issue state", err)
-
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
-
err = db.CloseIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to close issue", err)
-
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
return
} else {
log.Println("user is not permitted to close issue")
···
}
}
-
func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
-
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
-
s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
···
isIssueOwner := user.Did == issue.OwnerDid
if isCollaborator || isIssueOwner {
-
err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to reopen issue", err)
-
s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
return
}
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
return
} else {
log.Println("user is not the owner of the repo")
···
}
}
-
func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
case http.MethodPost:
body := r.FormValue("body")
if body == "" {
-
s.pages.Notice(w, "issue", "Body is required")
return
}
commentId := mathrand.IntN(1000000)
rkey := appview.TID()
-
err := db.NewIssueComment(s.db, &db.Comment{
OwnerDid: user.Did,
RepoAt: f.RepoAt,
Issue: issueIdInt,
···
})
if err != nil {
log.Println("failed to create comment", err)
-
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
createdAt := time.Now().Format(time.RFC3339)
commentIdInt64 := int64(commentId)
ownerDid := user.Did
-
issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue at", err)
-
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
atUri := f.RepoAt.String()
-
client, err := s.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
-
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
···
})
if err != nil {
log.Println("failed to create comment", err)
-
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
return
}
}
-
func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
-
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
-
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
-
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
return
}
-
identity, err := s.idResolver.ResolveIdent(r.Context(), comment.OwnerDid)
if err != nil {
log.Println("failed to resolve did")
return
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
-
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
DidHandleMap: didHandleMap,
···
})
}
-
func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
-
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
-
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
-
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
return
···
switch r.Method {
case http.MethodGet:
-
s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
Issue: issue,
···
case http.MethodPost:
// extract form value
newBody := r.FormValue("body")
-
client, err := s.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
-
s.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
rkey := comment.Rkey
// optimistic update
edited := time.Now()
-
err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
if err != nil {
log.Println("failed to perferom update-description query", err)
-
s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
}
···
if err != nil {
// failed to get record
log.Println(err, rkey)
-
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
return
}
value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json
···
comment.Edited = &edited
// return new comment body with htmx
-
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
DidHandleMap: didHandleMap,
···
}
-
func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
-
issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
-
s.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
···
return
}
-
comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
return
···
// optimistic deletion
deleted := time.Now()
-
err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
log.Println("failed to delete comment")
-
s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
return
}
// delete from pds
if comment.Rkey != "" {
-
client, err := s.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
-
s.pages.Notice(w, "issue-comment", "Failed to delete comment.")
return
}
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
···
comment.Deleted = &deleted
// htmx fragment of comment after deletion
-
s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
DidHandleMap: didHandleMap,
···
return
}
-
func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
state := params.Get("state")
isOpen := true
···
page = pagination.FirstPage()
}
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
issues, err := db.GetIssues(s.db, f.RepoAt, isOpen, page)
if err != nil {
log.Println("failed to get issues", err)
-
s.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
return
}
···
for i, issue := range issues {
identsToResolve[i] = issue.OwnerDid
}
-
resolvedIds := s.idResolver.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.oauth.GetUser(r),
RepoInfo: f.RepoInfo(user),
Issues: issues,
DidHandleMap: didHandleMap,
···
return
}
-
func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
switch r.Method {
case http.MethodGet:
-
s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
})
···
body := r.FormValue("body")
if title == "" || body == "" {
-
s.pages.Notice(w, "issues", "Title and body are required")
return
}
-
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
-
s.pages.Notice(w, "issues", "Failed to create issue, try again later")
return
}
···
})
if err != nil {
log.Println("failed to create issue", err)
-
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
-
issueId, err := db.GetIssueId(s.db, f.RepoAt)
if err != nil {
log.Println("failed to get issue id", err)
-
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
-
client, err := s.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
-
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
atUri := f.RepoAt.String()
···
})
if err != nil {
log.Println("failed to create issue", err)
-
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
-
err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri)
if err != nil {
log.Println("failed to set issue at", err)
-
s.pages.Notice(w, "issues", "Failed to create issue.")
return
}
-
if !s.config.Core.Dev {
-
err = s.posthog.Enqueue(posthog.Capture{
DistinctId: user.Did,
Event: "new_issue",
Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "issue_id": issueId},
···
}
}
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId))
return
}
}
-
func (s *State) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Printf("failed to resolve source repo: %v", err)
return
···
switch r.Method {
case http.MethodPost:
-
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
-
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot))
return
}
-
client, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev)
if err != nil {
-
s.pages.Notice(w, "repo", "Failed to reach knot server.")
return
}
var uri string
-
if s.config.Core.Dev {
uri = "http"
} else {
uri = "https"
···
_, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref)
if err != nil {
-
s.pages.Notice(w, "repo", "Failed to sync repository fork.")
return
}
-
s.pages.HxRefresh(w)
return
}
}
-
func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Printf("failed to resolve source repo: %v", err)
return
···
switch r.Method {
case http.MethodGet:
-
user := s.oauth.GetUser(r)
-
knots, err := s.enforcer.GetDomainsForUser(user.Did)
if err != nil {
-
s.pages.Notice(w, "repo", "Invalid user account.")
return
}
-
s.pages.ForkRepo(w, pages.ForkRepoParams{
LoggedInUser: user,
Knots: knots,
RepoInfo: f.RepoInfo(user),
···
knot := r.FormValue("knot")
if knot == "" {
-
s.pages.Notice(w, "repo", "Invalid form submission&mdash;missing knot domain.")
return
}
-
ok, err := s.enforcer.E.Enforce(user.Did, knot, knot, "repo:create")
if err != nil || !ok {
-
s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
return
}
···
// 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 err != nil {
if errors.Is(err, sql.ErrNoRows) {
// no existing repo with this name found, we can use the name as is
} else {
log.Println("error fetching existing repo from db", err)
-
s.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.")
return
}
} 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)
if err != nil {
-
s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot))
return
}
-
client, err := knotclient.NewSignedClient(knot, secret, s.config.Core.Dev)
if err != nil {
-
s.pages.Notice(w, "repo", "Failed to reach knot server.")
return
}
var uri string
-
if s.config.Core.Dev {
uri = "http"
} else {
uri = "https"
···
Source: sourceAt,
}
-
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println(err)
-
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
}
defer func() {
tx.Rollback()
-
err = s.enforcer.E.LoadPolicy()
if err != nil {
log.Println("failed to rollback policies")
}
···
resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName)
if err != nil {
-
s.pages.Notice(w, "repo", "Failed to create repository on knot server.")
return
}
switch resp.StatusCode {
case http.StatusConflict:
-
s.pages.Notice(w, "repo", "A repository with that name already exists.")
return
case http.StatusInternalServerError:
-
s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
case http.StatusNoContent:
// continue
}
-
xrpcClient, err := s.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
-
s.pages.Notice(w, "repo", "Failed to create repository.")
return
}
···
})
if err != nil {
log.Printf("failed to create record: %s", err)
-
s.pages.Notice(w, "repo", "Failed to announce repository creation.")
return
}
log.Println("created repo record: ", atresp.Uri)
···
err = db.AddRepo(tx, repo)
if err != nil {
log.Println(err)
-
s.pages.Notice(w, "repo", "Failed to save repository information.")
return
}
// acls
p, _ := securejoin.SecureJoin(user.Did, forkName)
-
err = s.enforcer.AddRepo(user.Did, knot, p)
if err != nil {
log.Println(err)
-
s.pages.Notice(w, "repo", "Failed to set up repository permissions.")
return
}
···
return
}
-
err = s.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
-
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, forkName))
return
}
}
-
func (s *State) RepoCompareNew(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
-
s.pages.Error503(w)
return
}
result, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
-
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
log.Println("failed to reach knotserver", err)
return
}
···
tags, err := us.Tags(f.OwnerDid(), f.RepoName)
if err != nil {
-
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
log.Println("failed to reach knotserver", err)
return
}
repoinfo := f.RepoInfo(user)
-
s.pages.RepoCompareNew(w, pages.RepoCompareNewParams{
LoggedInUser: user,
RepoInfo: repoinfo,
Branches: branches,
···
})
}
-
func (s *State) RepoCompare(w http.ResponseWriter, r *http.Request) {
-
user := s.oauth.GetUser(r)
-
f, err := s.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
if base == "" || head == "" {
log.Printf("invalid comparison")
-
s.pages.Error404(w)
return
}
-
us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
-
s.pages.Error503(w)
return
}
branches, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
-
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
log.Println("failed to reach knotserver", err)
return
}
tags, err := us.Tags(f.OwnerDid(), f.RepoName)
if err != nil {
-
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
log.Println("failed to reach knotserver", err)
return
}
formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head)
if err != nil {
-
s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
log.Println("failed to compare", err)
return
}
···
repoinfo := f.RepoInfo(user)
-
s.pages.RepoCompare(w, pages.RepoCompareParams{
LoggedInUser: user,
RepoInfo: repoinfo,
Branches: branches.Branches,
···
+
package repo
import (
"database/sql"
···
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/appview"
"tangled.sh/tangled.sh/core/appview/db"
+
"tangled.sh/tangled.sh/core/appview/idresolver"
"tangled.sh/tangled.sh/core/appview/oauth"
"tangled.sh/tangled.sh/core/appview/pages"
"tangled.sh/tangled.sh/core/appview/pages/markup"
···
"tangled.sh/tangled.sh/core/appview/reporesolver"
"tangled.sh/tangled.sh/core/knotclient"
"tangled.sh/tangled.sh/core/patchutil"
+
"tangled.sh/tangled.sh/core/rbac"
"tangled.sh/tangled.sh/core/types"
"github.com/bluesky-social/indigo/atproto/data"
···
lexutil "github.com/bluesky-social/indigo/lex/util"
)
+
type Repo struct {
+
repoResolver *reporesolver.RepoResolver
+
idResolver *idresolver.Resolver
+
config *appview.Config
+
oauth *oauth.OAuth
+
pages *pages.Pages
+
db *db.DB
+
enforcer *rbac.Enforcer
+
posthog posthog.Client
+
}
+
+
func New(
+
oauth *oauth.OAuth,
+
repoResolver *reporesolver.RepoResolver,
+
pages *pages.Pages,
+
idResolver *idresolver.Resolver,
+
db *db.DB,
+
config *appview.Config,
+
posthog posthog.Client,
+
enforcer *rbac.Enforcer,
+
) *Repo {
+
return &Repo{oauth: oauth,
+
repoResolver: repoResolver,
+
pages: pages,
+
idResolver: idResolver,
+
config: config,
+
db: db,
+
posthog: posthog,
+
enforcer: enforcer,
+
}
+
}
+
+
func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) {
ref := chi.URLParam(r, "ref")
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
return
}
+
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
+
rp.pages.Error503(w)
return
}
result, err := us.Index(f.OwnerDid(), f.RepoName, ref)
if err != nil {
+
rp.pages.Error503(w)
log.Println("failed to reach knotserver", err)
return
}
···
emails := uniqueEmails(commitsTrunc)
+
user := rp.oauth.GetUser(r)
repoInfo := f.RepoInfo(user)
+
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
if err != nil {
log.Printf("failed to get registration key for %s: %s", f.Knot, err)
+
rp.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
}
+
signedClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
if err != nil {
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
return
···
var forkInfo *types.ForkInfo
if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) {
+
forkInfo, err = getForkInfo(repoInfo, rp, f, user, signedClient)
if err != nil {
log.Printf("Failed to fetch fork information: %v", err)
return
···
// non-fatal
}
+
rp.pages.RepoIndexPage(w, pages.RepoIndexParams{
LoggedInUser: user,
RepoInfo: repoInfo,
TagMap: tagMap,
···
TagsTrunc: tagsTrunc,
ForkInfo: forkInfo,
BranchesTrunc: branchesTrunc,
+
EmailToDidOrHandle: EmailToDidOrHandle(rp, emails),
Languages: repoLanguages,
})
return
···
func getForkInfo(
repoInfo repoinfo.RepoInfo,
+
rp *Repo,
f *reporesolver.ResolvedRepo,
user *oauth.User,
signedClient *knotclient.SignedClient,
···
return &forkInfo, nil
}
+
us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, rp.config.Core.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot)
return nil, err
···
return &forkInfo, nil
}
+
func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
return
···
ref := chi.URLParam(r, "ref")
+
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
return
···
tagMap[hash] = append(tagMap[hash], tag.Name)
}
+
user := rp.oauth.GetUser(r)
+
rp.pages.RepoLog(w, pages.RepoLogParams{
LoggedInUser: user,
TagMap: tagMap,
RepoInfo: f.RepoInfo(user),
RepoLogResponse: *repolog,
+
EmailToDidOrHandle: EmailToDidOrHandle(rp, uniqueEmails(repolog.Commits)),
})
return
}
+
func (rp *Repo) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
w.WriteHeader(http.StatusBadRequest)
return
}
+
user := rp.oauth.GetUser(r)
+
rp.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{
RepoInfo: f.RepoInfo(user),
})
return
}
+
func (rp *Repo) RepoDescription(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
w.WriteHeader(http.StatusBadRequest)
···
return
}
+
user := rp.oauth.GetUser(r)
switch r.Method {
case http.MethodGet:
+
rp.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
RepoInfo: f.RepoInfo(user),
})
return
case http.MethodPut:
+
user := rp.oauth.GetUser(r)
newDescription := r.FormValue("description")
+
client, err := rp.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get client")
+
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
}
// optimistic update
+
err = db.UpdateDescription(rp.db, string(repoAt), newDescription)
if err != nil {
log.Println("failed to perferom update-description query", err)
+
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
}
···
ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey)
if err != nil {
// failed to get record
+
rp.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.")
return
}
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
···
if err != nil {
log.Println("failed to perferom update-description query", err)
// failed to get record
+
rp.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.")
return
}
newRepoInfo := f.RepoInfo(user)
newRepoInfo.Description = newDescription
+
rp.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{
RepoInfo: newRepoInfo,
})
return
}
}
+
func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
return
}
ref := chi.URLParam(r, "ref")
protocol := "http"
+
if !rp.config.Core.Dev {
protocol = "https"
}
if !plumbing.IsHash(ref) {
+
rp.pages.Error404(w)
return
}
···
return
}
+
user := rp.oauth.GetUser(r)
+
rp.pages.RepoCommit(w, pages.RepoCommitParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
RepoCommitResponse: result,
+
EmailToDidOrHandle: EmailToDidOrHandle(rp, []string{result.Diff.Commit.Author.Email}),
})
return
}
+
func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to fully resolve repo", err)
return
···
ref := chi.URLParam(r, "ref")
treePath := chi.URLParam(r, "*")
protocol := "http"
+
if !rp.config.Core.Dev {
protocol = "https"
}
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath))
···
return
}
+
user := rp.oauth.GetUser(r)
var breadcrumbs [][]string
breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)})
···
baseTreeLink := path.Join(f.OwnerSlashRepo(), "tree", ref, treePath)
baseBlobLink := path.Join(f.OwnerSlashRepo(), "blob", ref, treePath)
+
rp.pages.RepoTree(w, pages.RepoTreeParams{
LoggedInUser: user,
BreadCrumbs: breadcrumbs,
BaseTreeLink: baseTreeLink,
···
return
}
+
func (rp *Repo) RepoTags(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
return
···
return
}
+
artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt))
if err != nil {
log.Println("failed grab artifacts", err)
return
···
}
}
+
user := rp.oauth.GetUser(r)
+
rp.pages.RepoTags(w, pages.RepoTagsParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
RepoTagsResponse: *result,
···
return
}
+
func (rp *Repo) RepoBranches(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
return
···
return strings.Compare(a.Name, b.Name) * -1
})
+
user := rp.oauth.GetUser(r)
+
rp.pages.RepoBranches(w, pages.RepoBranchesParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
RepoBranchesResponse: *result,
···
return
}
+
func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
ref := chi.URLParam(r, "ref")
filePath := chi.URLParam(r, "*")
protocol := "http"
+
if !rp.config.Core.Dev {
protocol = "https"
}
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
···
showRendered = r.URL.Query().Get("code") != "true"
}
+
user := rp.oauth.GetUser(r)
+
rp.pages.RepoBlob(w, pages.RepoBlobParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
RepoBlobResponse: result,
···
return
}
+
func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
filePath := chi.URLParam(r, "*")
protocol := "http"
+
if !rp.config.Core.Dev {
protocol = "https"
}
resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath))
···
return
}
+
func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
+
collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator)
if err != nil {
w.Write([]byte("failed to resolve collaborator did to a handle"))
return
···
// TODO: create an atproto record for this
+
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
return
}
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
return
···
return
}
+
tx, err := rp.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
···
}
defer func() {
tx.Rollback()
+
err = rp.enforcer.E.LoadPolicy()
if err != nil {
log.Println("failed to rollback policies")
}
}()
+
err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo())
if err != nil {
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
}
+
err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot)
if err != nil {
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
return
···
return
}
+
err = rp.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
···
}
+
func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
// remove record from pds
+
xrpcClient, err := rp.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
return
···
})
if err != nil {
log.Printf("failed to delete record: %s", err)
+
rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.")
return
}
log.Println("removed repo record ", f.RepoAt.String())
+
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
return
}
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
return
···
log.Println("removed repo from knot ", f.Knot)
}
+
tx, err := rp.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println("failed to start tx")
w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err)))
···
}
defer func() {
tx.Rollback()
+
err = rp.enforcer.E.LoadPolicy()
if err != nil {
log.Println("failed to rollback policies")
}
}()
// remove collaborator RBAC
+
repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot)
if err != nil {
+
rp.pages.Notice(w, "settings-delete", "Failed to remove collaborators")
return
}
for _, c := range repoCollaborators {
did := c[0]
+
rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo())
}
log.Println("removed collaborators")
// remove repo RBAC
+
err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo())
if err != nil {
+
rp.pages.Notice(w, "settings-delete", "Failed to update RBAC rules")
return
}
// remove repo from db
err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName)
if err != nil {
+
rp.pages.Notice(w, "settings-delete", "Failed to update appview")
return
}
log.Println("removed repo from db")
···
return
}
+
err = rp.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
+
rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid()))
}
+
func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
+
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
if err != nil {
log.Printf("no key found for domain %s: %s\n", f.Knot, err)
return
}
+
ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
if err != nil {
log.Println("failed to create client to ", f.Knot)
return
···
}
if ksResp.StatusCode != http.StatusNoContent {
+
rp.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.")
return
}
w.Write([]byte(fmt.Sprint("default branch set to: ", branch)))
}
+
func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) {
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
switch r.Method {
case http.MethodGet:
// for now, this is just pubkeys
+
user := rp.oauth.GetUser(r)
repoCollaborators, err := f.Collaborators(r.Context())
if err != nil {
log.Println("failed to get collaborators", err)
···
isCollaboratorInviteAllowed := false
if user != nil {
+
ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo())
if err == nil && ok {
isCollaboratorInviteAllowed = true
}
}
+
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
if err != nil {
log.Println("failed to create unsigned client", err)
return
···
return
}
+
rp.pages.RepoSettings(w, pages.RepoSettingsParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
Collaborators: repoCollaborators,
···
}
}
+
func (rp *Repo) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
+
issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue and comments", err)
+
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
+
issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid)
if err != nil {
log.Println("failed to resolve issue owner", err)
}
···
for i, comment := range comments {
identsToResolve[i] = comment.OwnerDid
}
+
resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
}
}
+
rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
Issue: *issue,
···
}
+
func (rp *Repo) CloseIssue(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
+
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
+
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
···
closed := tangled.RepoIssueStateClosed
+
client, err := rp.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
return
···
if err != nil {
log.Println("failed to update issue state", err)
+
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
+
err = db.CloseIssue(rp.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to close issue", err)
+
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
return
} else {
log.Println("user is not permitted to close issue")
···
}
}
+
func (rp *Repo) ReopenIssue(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
+
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
+
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
return
}
···
isIssueOwner := user.Did == issue.OwnerDid
if isCollaborator || isIssueOwner {
+
err := db.ReopenIssue(rp.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to reopen issue", err)
+
rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
return
}
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
return
} else {
log.Println("user is not the owner of the repo")
···
}
}
+
func (rp *Repo) NewIssueComment(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
case http.MethodPost:
body := r.FormValue("body")
if body == "" {
+
rp.pages.Notice(w, "issue", "Body is required")
return
}
commentId := mathrand.IntN(1000000)
rkey := appview.TID()
+
err := db.NewIssueComment(rp.db, &db.Comment{
OwnerDid: user.Did,
RepoAt: f.RepoAt,
Issue: issueIdInt,
···
})
if err != nil {
log.Println("failed to create comment", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
createdAt := time.Now().Format(time.RFC3339)
commentIdInt64 := int64(commentId)
ownerDid := user.Did
+
issueAt, err := db.GetIssueAt(rp.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue at", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
atUri := f.RepoAt.String()
+
client, err := rp.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
···
})
if err != nil {
log.Println("failed to create comment", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
return
}
}
+
func (rp *Repo) IssueComment(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
+
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
+
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
+
comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
return
}
+
identity, err := rp.idResolver.ResolveIdent(r.Context(), comment.OwnerDid)
if err != nil {
log.Println("failed to resolve did")
return
···
didHandleMap[identity.DID.String()] = identity.DID.String()
}
+
rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
DidHandleMap: didHandleMap,
···
})
}
+
func (rp *Repo) EditIssueComment(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
+
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
+
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
+
comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
return
···
switch r.Method {
case http.MethodGet:
+
rp.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
Issue: issue,
···
case http.MethodPost:
// extract form value
newBody := r.FormValue("body")
+
client, err := rp.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
return
}
rkey := comment.Rkey
// optimistic update
edited := time.Now()
+
err = db.EditComment(rp.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
if err != nil {
log.Println("failed to perferom update-description query", err)
+
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
return
}
···
if err != nil {
// failed to get record
log.Println(err, rkey)
+
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.")
return
}
value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json
···
comment.Edited = &edited
// return new comment body with htmx
+
rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
DidHandleMap: didHandleMap,
···
}
+
func (rp *Repo) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
return
}
+
issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt)
if err != nil {
log.Println("failed to get issue", err)
+
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
return
}
···
return
}
+
comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
http.Error(w, "bad comment id", http.StatusBadRequest)
return
···
// optimistic deletion
deleted := time.Now()
+
err = db.DeleteComment(rp.db, f.RepoAt, issueIdInt, commentIdInt)
if err != nil {
log.Println("failed to delete comment")
+
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
return
}
// delete from pds
if comment.Rkey != "" {
+
client, err := rp.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
+
rp.pages.Notice(w, "issue-comment", "Failed to delete comment.")
return
}
_, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{
···
comment.Deleted = &deleted
// htmx fragment of comment after deletion
+
rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
DidHandleMap: didHandleMap,
···
return
}
+
func (rp *Repo) RepoIssues(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
state := params.Get("state")
isOpen := true
···
page = pagination.FirstPage()
}
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
issues, err := db.GetIssues(rp.db, f.RepoAt, isOpen, page)
if err != nil {
log.Println("failed to get issues", err)
+
rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.")
return
}
···
for i, issue := range issues {
identsToResolve[i] = issue.OwnerDid
}
+
resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
if !identity.Handle.IsInvalidHandle() {
···
}
}
+
rp.pages.RepoIssues(w, pages.RepoIssuesParams{
+
LoggedInUser: rp.oauth.GetUser(r),
RepoInfo: f.RepoInfo(user),
Issues: issues,
DidHandleMap: didHandleMap,
···
return
}
+
func (rp *Repo) NewIssue(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
switch r.Method {
case http.MethodGet:
+
rp.pages.RepoNewIssue(w, pages.RepoNewIssueParams{
LoggedInUser: user,
RepoInfo: f.RepoInfo(user),
})
···
body := r.FormValue("body")
if title == "" || body == "" {
+
rp.pages.Notice(w, "issues", "Title and body are required")
return
}
+
tx, err := rp.db.BeginTx(r.Context(), nil)
if err != nil {
+
rp.pages.Notice(w, "issues", "Failed to create issue, try again later")
return
}
···
})
if err != nil {
log.Println("failed to create issue", err)
+
rp.pages.Notice(w, "issues", "Failed to create issue.")
return
}
+
issueId, err := db.GetIssueId(rp.db, f.RepoAt)
if err != nil {
log.Println("failed to get issue id", err)
+
rp.pages.Notice(w, "issues", "Failed to create issue.")
return
}
+
client, err := rp.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
+
rp.pages.Notice(w, "issues", "Failed to create issue.")
return
}
atUri := f.RepoAt.String()
···
})
if err != nil {
log.Println("failed to create issue", err)
+
rp.pages.Notice(w, "issues", "Failed to create issue.")
return
}
+
err = db.SetIssueAt(rp.db, f.RepoAt, issueId, resp.Uri)
if err != nil {
log.Println("failed to set issue at", err)
+
rp.pages.Notice(w, "issues", "Failed to create issue.")
return
}
+
if !rp.config.Core.Dev {
+
err = rp.posthog.Enqueue(posthog.Capture{
DistinctId: user.Did,
Event: "new_issue",
Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "issue_id": issueId},
···
}
}
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId))
return
}
}
+
func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Printf("failed to resolve source repo: %v", err)
return
···
switch r.Method {
case http.MethodPost:
+
secret, err := db.GetRegistrationKey(rp.db, f.Knot)
if err != nil {
+
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", f.Knot))
return
}
+
client, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev)
if err != nil {
+
rp.pages.Notice(w, "repo", "Failed to reach knot server.")
return
}
var uri string
+
if rp.config.Core.Dev {
uri = "http"
} else {
uri = "https"
···
_, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref)
if err != nil {
+
rp.pages.Notice(w, "repo", "Failed to sync repository fork.")
return
}
+
rp.pages.HxRefresh(w)
return
}
}
+
func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Printf("failed to resolve source repo: %v", err)
return
···
switch r.Method {
case http.MethodGet:
+
user := rp.oauth.GetUser(r)
+
knots, err := rp.enforcer.GetDomainsForUser(user.Did)
if err != nil {
+
rp.pages.Notice(w, "repo", "Invalid user account.")
return
}
+
rp.pages.ForkRepo(w, pages.ForkRepoParams{
LoggedInUser: user,
Knots: knots,
RepoInfo: f.RepoInfo(user),
···
knot := r.FormValue("knot")
if knot == "" {
+
rp.pages.Notice(w, "repo", "Invalid form submission&mdash;missing knot domain.")
return
}
+
ok, err := rp.enforcer.E.Enforce(user.Did, knot, knot, "repo:create")
if err != nil || !ok {
+
rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.")
return
}
···
// this check is *only* to see if the forked repo name already exists
// in the user's account.
+
existingRepo, err := db.GetRepo(rp.db, user.Did, f.RepoName)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// no existing repo with this name found, we can use the name as is
} else {
log.Println("error fetching existing repo from db", err)
+
rp.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.")
return
}
} 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(rp.db, knot)
if err != nil {
+
rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", knot))
return
}
+
client, err := knotclient.NewSignedClient(knot, secret, rp.config.Core.Dev)
if err != nil {
+
rp.pages.Notice(w, "repo", "Failed to reach knot server.")
return
}
var uri string
+
if rp.config.Core.Dev {
uri = "http"
} else {
uri = "https"
···
Source: sourceAt,
}
+
tx, err := rp.db.BeginTx(r.Context(), nil)
if err != nil {
log.Println(err)
+
rp.pages.Notice(w, "repo", "Failed to save repository information.")
return
}
defer func() {
tx.Rollback()
+
err = rp.enforcer.E.LoadPolicy()
if err != nil {
log.Println("failed to rollback policies")
}
···
resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName)
if err != nil {
+
rp.pages.Notice(w, "repo", "Failed to create repository on knot server.")
return
}
switch resp.StatusCode {
case http.StatusConflict:
+
rp.pages.Notice(w, "repo", "A repository with that name already exists.")
return
case http.StatusInternalServerError:
+
rp.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.")
case http.StatusNoContent:
// continue
}
+
xrpcClient, err := rp.oauth.AuthorizedClient(r)
if err != nil {
log.Println("failed to get authorized client", err)
+
rp.pages.Notice(w, "repo", "Failed to create repository.")
return
}
···
})
if err != nil {
log.Printf("failed to create record: %s", err)
+
rp.pages.Notice(w, "repo", "Failed to announce repository creation.")
return
}
log.Println("created repo record: ", atresp.Uri)
···
err = db.AddRepo(tx, repo)
if err != nil {
log.Println(err)
+
rp.pages.Notice(w, "repo", "Failed to save repository information.")
return
}
// acls
p, _ := securejoin.SecureJoin(user.Did, forkName)
+
err = rp.enforcer.AddRepo(user.Did, knot, p)
if err != nil {
log.Println(err)
+
rp.pages.Notice(w, "repo", "Failed to set up repository permissions.")
return
}
···
return
}
+
err = rp.enforcer.E.SavePolicy()
if err != nil {
log.Println("failed to update ACLs", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
+
rp.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, forkName))
return
}
}
+
func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
}
+
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
+
rp.pages.Error503(w)
return
}
result, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
log.Println("failed to reach knotserver", err)
return
}
···
tags, err := us.Tags(f.OwnerDid(), f.RepoName)
if err != nil {
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
log.Println("failed to reach knotserver", err)
return
}
repoinfo := f.RepoInfo(user)
+
rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{
LoggedInUser: user,
RepoInfo: repoinfo,
Branches: branches,
···
})
}
+
func (rp *Repo) RepoCompare(w http.ResponseWriter, r *http.Request) {
+
user := rp.oauth.GetUser(r)
+
f, err := rp.repoResolver.Resolve(r)
if err != nil {
log.Println("failed to get repo and knot", err)
return
···
if base == "" || head == "" {
log.Printf("invalid comparison")
+
rp.pages.Error404(w)
return
}
+
us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev)
if err != nil {
log.Printf("failed to create unsigned client for %s", f.Knot)
+
rp.pages.Error503(w)
return
}
branches, err := us.Branches(f.OwnerDid(), f.RepoName)
if err != nil {
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
log.Println("failed to reach knotserver", err)
return
}
tags, err := us.Tags(f.OwnerDid(), f.RepoName)
if err != nil {
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
log.Println("failed to reach knotserver", err)
return
}
formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head)
if err != nil {
+
rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.")
log.Println("failed to compare", err)
return
}
···
repoinfo := f.RepoInfo(user)
+
rp.pages.RepoCompare(w, pages.RepoCompareParams{
LoggedInUser: user,
RepoInfo: repoinfo,
Branches: branches.Branches,
+4 -4
appview/state/repo_util.go appview/repo/repo_util.go
···
-
package state
import (
"context"
···
return
}
-
func EmailToDidOrHandle(s *State, emails []string) map[string]string {
-
emailToDid, err := db.GetEmailToDid(s.db, emails, true) // only get verified emails for mapping
if err != nil {
log.Printf("error fetching dids for emails: %v", err)
return nil
···
for _, v := range emailToDid {
dids = append(dids, v)
}
-
resolvedIdents := s.idResolver.ResolveIdents(context.Background(), dids)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIdents {
···
+
package repo
import (
"context"
···
return
}
+
func EmailToDidOrHandle(r *Repo, emails []string) map[string]string {
+
emailToDid, err := db.GetEmailToDid(r.db, emails, true) // only get verified emails for mapping
if err != nil {
log.Printf("error fetching dids for emails: %v", err)
return nil
···
for _, v := range emailToDid {
dids = append(dids, v)
}
+
resolvedIdents := r.idResolver.ResolveIdents(context.Background(), dids)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIdents {
+7 -85
appview/state/router.go
···
"tangled.sh/tangled.sh/core/appview/middleware"
oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler"
"tangled.sh/tangled.sh/core/appview/pulls"
"tangled.sh/tangled.sh/core/appview/settings"
"tangled.sh/tangled.sh/core/appview/state/userutil"
)
···
r.With(mw.ResolveRepo()).Route("/{repo}", func(r chi.Router) {
r.Use(mw.GoImport())
-
r.Get("/", s.RepoIndex)
-
r.Get("/commits/{ref}", s.RepoLog)
-
r.Route("/tree/{ref}", func(r chi.Router) {
-
r.Get("/", s.RepoIndex)
-
r.Get("/*", s.RepoTree)
-
})
-
r.Get("/commit/{ref}", s.RepoCommit)
-
r.Get("/branches", s.RepoBranches)
-
r.Route("/tags", func(r chi.Router) {
-
r.Get("/", s.RepoTags)
-
r.Route("/{tag}", func(r chi.Router) {
-
r.Use(middleware.AuthMiddleware(s.oauth))
-
// require auth to download for now
-
r.Get("/download/{file}", s.DownloadArtifact)
-
-
// require repo:push to upload or delete artifacts
-
//
-
// additionally: only the uploader can truly delete an artifact
-
// (record+blob will live on their pds)
-
r.Group(func(r chi.Router) {
-
r.With(mw.RepoPermissionMiddleware("repo:push"))
-
r.Post("/upload", s.AttachArtifact)
-
r.Delete("/{file}", s.DeleteArtifact)
-
})
-
})
-
})
-
r.Get("/blob/{ref}/*", s.RepoBlob)
-
r.Get("/raw/{ref}/*", s.RepoBlobRaw)
-
-
r.Route("/issues", func(r chi.Router) {
-
r.With(middleware.Paginate).Get("/", s.RepoIssues)
-
r.Get("/{issue}", s.RepoSingleIssue)
-
-
r.Group(func(r chi.Router) {
-
r.Use(middleware.AuthMiddleware(s.oauth))
-
r.Get("/new", s.NewIssue)
-
r.Post("/new", s.NewIssue)
-
r.Post("/{issue}/comment", s.NewIssueComment)
-
r.Route("/{issue}/comment/{comment_id}/", func(r chi.Router) {
-
r.Get("/", s.IssueComment)
-
r.Delete("/", s.DeleteIssueComment)
-
r.Get("/edit", s.EditIssueComment)
-
r.Post("/edit", s.EditIssueComment)
-
})
-
r.Post("/{issue}/close", s.CloseIssue)
-
r.Post("/{issue}/reopen", s.ReopenIssue)
-
})
-
})
-
-
r.Route("/fork", func(r chi.Router) {
-
r.Use(middleware.AuthMiddleware(s.oauth))
-
r.Get("/", s.ForkRepo)
-
r.Post("/", s.ForkRepo)
-
r.With(mw.RepoPermissionMiddleware("repo:owner")).Route("/sync", func(r chi.Router) {
-
r.Post("/", s.SyncRepoFork)
-
})
-
})
-
-
r.Route("/compare", func(r chi.Router) {
-
r.Get("/", s.RepoCompareNew) // start an new comparison
-
-
// we have to wildcard here since we want to support GitHub's compare syntax
-
// /compare/{ref1}...{ref2}
-
// for example:
-
// /compare/master...some/feature
-
// /compare/master...example.com:another/feature <- this is a fork
-
r.Get("/{base}/{head}", s.RepoCompare)
-
r.Get("/*", s.RepoCompare)
-
})
r.Mount("/pulls", s.PullsRouter(mw))
···
r.Post("/git-upload-pack", s.UploadPack)
r.Post("/git-receive-pack", s.ReceivePack)
-
// settings routes, needs auth
-
r.Group(func(r chi.Router) {
-
r.Use(middleware.AuthMiddleware(s.oauth))
-
// repo description can only be edited by owner
-
r.With(mw.RepoPermissionMiddleware("repo:owner")).Route("/description", func(r chi.Router) {
-
r.Put("/", s.RepoDescription)
-
r.Get("/", s.RepoDescription)
-
r.Get("/edit", s.RepoDescriptionEdit)
-
})
-
r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) {
-
r.Get("/", s.RepoSettings)
-
r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", s.AddCollaborator)
-
r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", s.DeleteRepo)
-
r.Put("/branches/default", s.SetDefaultBranch)
-
})
-
})
})
})
···
pulls := pulls.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config)
return pulls.Router(mw)
}
···
"tangled.sh/tangled.sh/core/appview/middleware"
oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler"
"tangled.sh/tangled.sh/core/appview/pulls"
+
"tangled.sh/tangled.sh/core/appview/repo"
"tangled.sh/tangled.sh/core/appview/settings"
"tangled.sh/tangled.sh/core/appview/state/userutil"
)
···
r.With(mw.ResolveRepo()).Route("/{repo}", func(r chi.Router) {
r.Use(mw.GoImport())
+
r.Mount("/", s.RepoRouter(mw))
r.Mount("/pulls", s.PullsRouter(mw))
···
r.Post("/git-upload-pack", s.UploadPack)
r.Post("/git-receive-pack", s.ReceivePack)
})
})
···
pulls := pulls.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config)
return pulls.Router(mw)
}
+
+
func (s *State) RepoRouter(mw *middleware.Middleware) http.Handler {
+
repo := repo.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.posthog, s.enforcer)
+
return repo.Router(mw)
+
}