Monorepo for Tangled tangled.org

appview: repo: refactor into its own package

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

authored by anirudh.fi and committed by Tangled 4271d095 28184900

Changed files
+460 -390
appview
+16 -2
appview/pulls/pulls.go
··· 43 posthog posthog.Client 44 } 45 46 - func New(oauth *oauth.OAuth, repoResolver *reporesolver.RepoResolver, pages *pages.Pages, resolver *idresolver.Resolver, db *db.DB, config *appview.Config) *Pulls { 47 - return &Pulls{oauth: oauth, repoResolver: repoResolver, pages: pages, idResolver: resolver, db: db, config: config} 48 } 49 50 // htmx fragment
··· 43 posthog posthog.Client 44 } 45 46 + func New( 47 + oauth *oauth.OAuth, 48 + repoResolver *reporesolver.RepoResolver, 49 + pages *pages.Pages, 50 + resolver *idresolver.Resolver, 51 + db *db.DB, 52 + config *appview.Config, 53 + ) *Pulls { 54 + return &Pulls{ 55 + oauth: oauth, 56 + repoResolver: repoResolver, 57 + pages: pages, 58 + idResolver: resolver, 59 + db: db, 60 + config: config, 61 + } 62 } 63 64 // htmx fragment
+100
appview/repo/router.go
···
··· 1 + package repo 2 + 3 + import ( 4 + "net/http" 5 + 6 + "github.com/go-chi/chi/v5" 7 + "tangled.sh/tangled.sh/core/appview/middleware" 8 + ) 9 + 10 + func (rp *Repo) Router(mw *middleware.Middleware) http.Handler { 11 + r := chi.NewRouter() 12 + r.Get("/", rp.RepoIndex) 13 + r.Get("/commits/{ref}", rp.RepoLog) 14 + r.Route("/tree/{ref}", func(r chi.Router) { 15 + r.Get("/", rp.RepoIndex) 16 + r.Get("/*", rp.RepoTree) 17 + }) 18 + r.Get("/commit/{ref}", rp.RepoCommit) 19 + r.Get("/branches", rp.RepoBranches) 20 + r.Route("/tags", func(r chi.Router) { 21 + r.Get("/", rp.RepoTags) 22 + r.Route("/{tag}", func(r chi.Router) { 23 + r.Use(middleware.AuthMiddleware(rp.oauth)) 24 + // require auth to download for now 25 + r.Get("/download/{file}", rp.DownloadArtifact) 26 + 27 + // require repo:push to upload or delete artifacts 28 + // 29 + // additionally: only the uploader can truly delete an artifact 30 + // (record+blob will live on their pds) 31 + r.Group(func(r chi.Router) { 32 + r.With(mw.RepoPermissionMiddleware("repo:push")) 33 + r.Post("/upload", rp.AttachArtifact) 34 + r.Delete("/{file}", rp.DeleteArtifact) 35 + }) 36 + }) 37 + }) 38 + r.Get("/blob/{ref}/*", rp.RepoBlob) 39 + r.Get("/raw/{ref}/*", rp.RepoBlobRaw) 40 + 41 + r.Route("/issues", func(r chi.Router) { 42 + r.With(middleware.Paginate).Get("/", rp.RepoIssues) 43 + r.Get("/{issue}", rp.RepoSingleIssue) 44 + 45 + r.Group(func(r chi.Router) { 46 + r.Use(middleware.AuthMiddleware(rp.oauth)) 47 + r.Get("/new", rp.NewIssue) 48 + r.Post("/new", rp.NewIssue) 49 + r.Post("/{issue}/comment", rp.NewIssueComment) 50 + r.Route("/{issue}/comment/{comment_id}/", func(r chi.Router) { 51 + r.Get("/", rp.IssueComment) 52 + r.Delete("/", rp.DeleteIssueComment) 53 + r.Get("/edit", rp.EditIssueComment) 54 + r.Post("/edit", rp.EditIssueComment) 55 + }) 56 + r.Post("/{issue}/close", rp.CloseIssue) 57 + r.Post("/{issue}/reopen", rp.ReopenIssue) 58 + }) 59 + }) 60 + 61 + r.Route("/fork", func(r chi.Router) { 62 + r.Use(middleware.AuthMiddleware(rp.oauth)) 63 + r.Get("/", rp.ForkRepo) 64 + r.Post("/", rp.ForkRepo) 65 + r.With(mw.RepoPermissionMiddleware("repo:owner")).Route("/sync", func(r chi.Router) { 66 + r.Post("/", rp.SyncRepoFork) 67 + }) 68 + }) 69 + 70 + r.Route("/compare", func(r chi.Router) { 71 + r.Get("/", rp.RepoCompareNew) // start an new comparison 72 + 73 + // we have to wildcard here since we want to support GitHub's compare syntax 74 + // /compare/{ref1}...{ref2} 75 + // for example: 76 + // /compare/master...some/feature 77 + // /compare/master...example.com:another/feature <- this is a fork 78 + r.Get("/{base}/{head}", rp.RepoCompare) 79 + r.Get("/*", rp.RepoCompare) 80 + }) 81 + 82 + // settings routes, needs auth 83 + r.Group(func(r chi.Router) { 84 + r.Use(middleware.AuthMiddleware(rp.oauth)) 85 + // repo description can only be edited by owner 86 + r.With(mw.RepoPermissionMiddleware("repo:owner")).Route("/description", func(r chi.Router) { 87 + r.Put("/", rp.RepoDescription) 88 + r.Get("/", rp.RepoDescription) 89 + r.Get("/edit", rp.RepoDescriptionEdit) 90 + }) 91 + r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) { 92 + r.Get("/", rp.RepoSettings) 93 + r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", rp.AddCollaborator) 94 + r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", rp.DeleteRepo) 95 + r.Put("/branches/default", rp.SetDefaultBranch) 96 + }) 97 + }) 98 + 99 + return r 100 + }
+38 -38
appview/state/artifact.go appview/repo/artifact.go
··· 1 - package state 2 3 import ( 4 "fmt" ··· 23 ) 24 25 // TODO: proper statuses here on early exit 26 - func (s *State) AttachArtifact(w http.ResponseWriter, r *http.Request) { 27 - user := s.oauth.GetUser(r) 28 tagParam := chi.URLParam(r, "tag") 29 - f, err := s.repoResolver.Resolve(r) 30 if err != nil { 31 log.Println("failed to get repo and knot", err) 32 - s.pages.Notice(w, "upload", "failed to upload artifact, error in repo resolution") 33 return 34 } 35 36 - tag, err := s.resolveTag(f, tagParam) 37 if err != nil { 38 log.Println("failed to resolve tag", err) 39 - s.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution") 40 return 41 } 42 43 file, handler, err := r.FormFile("artifact") 44 if err != nil { 45 log.Println("failed to upload artifact", err) 46 - s.pages.Notice(w, "upload", "failed to upload artifact") 47 return 48 } 49 defer file.Close() 50 51 - client, err := s.oauth.AuthorizedClient(r) 52 if err != nil { 53 log.Println("failed to get authorized client", err) 54 - s.pages.Notice(w, "upload", "failed to get authorized client") 55 return 56 } 57 58 uploadBlobResp, err := client.RepoUploadBlob(r.Context(), file) 59 if err != nil { 60 log.Println("failed to upload blob", err) 61 - s.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.") 62 return 63 } 64 ··· 83 }) 84 if err != nil { 85 log.Println("failed to create record", err) 86 - s.pages.Notice(w, "upload", "Failed to create artifact record. Try again later.") 87 return 88 } 89 90 log.Println(putRecordResp.Uri) 91 92 - tx, err := s.db.BeginTx(r.Context(), nil) 93 if err != nil { 94 log.Println("failed to start tx") 95 - s.pages.Notice(w, "upload", "Failed to create artifact. Try again later.") 96 return 97 } 98 defer tx.Rollback() ··· 112 err = db.AddArtifact(tx, artifact) 113 if err != nil { 114 log.Println("failed to add artifact record to db", err) 115 - s.pages.Notice(w, "upload", "Failed to create artifact. Try again later.") 116 return 117 } 118 119 err = tx.Commit() 120 if err != nil { 121 log.Println("failed to add artifact record to db") 122 - s.pages.Notice(w, "upload", "Failed to create artifact. Try again later.") 123 return 124 } 125 126 - s.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{ 127 LoggedInUser: user, 128 RepoInfo: f.RepoInfo(user), 129 Artifact: artifact, ··· 131 } 132 133 // TODO: proper statuses here on early exit 134 - func (s *State) DownloadArtifact(w http.ResponseWriter, r *http.Request) { 135 tagParam := chi.URLParam(r, "tag") 136 filename := chi.URLParam(r, "file") 137 - f, err := s.repoResolver.Resolve(r) 138 if err != nil { 139 log.Println("failed to get repo and knot", err) 140 return 141 } 142 143 - tag, err := s.resolveTag(f, tagParam) 144 if err != nil { 145 log.Println("failed to resolve tag", err) 146 - s.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution") 147 return 148 } 149 150 - client, err := s.oauth.AuthorizedClient(r) 151 if err != nil { 152 log.Println("failed to get authorized client", err) 153 return 154 } 155 156 artifacts, err := db.GetArtifact( 157 - s.db, 158 db.FilterEq("repo_at", f.RepoAt), 159 db.FilterEq("tag", tag.Tag.Hash[:]), 160 db.FilterEq("name", filename), ··· 181 } 182 183 // TODO: proper statuses here on early exit 184 - func (s *State) DeleteArtifact(w http.ResponseWriter, r *http.Request) { 185 - user := s.oauth.GetUser(r) 186 tagParam := chi.URLParam(r, "tag") 187 filename := chi.URLParam(r, "file") 188 - f, err := s.repoResolver.Resolve(r) 189 if err != nil { 190 log.Println("failed to get repo and knot", err) 191 return 192 } 193 194 - client, _ := s.oauth.AuthorizedClient(r) 195 196 tag := plumbing.NewHash(tagParam) 197 198 artifacts, err := db.GetArtifact( 199 - s.db, 200 db.FilterEq("repo_at", f.RepoAt), 201 db.FilterEq("tag", tag[:]), 202 db.FilterEq("name", filename), 203 ) 204 if err != nil { 205 log.Println("failed to get artifacts", err) 206 - s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 207 return 208 } 209 if len(artifacts) != 1 { 210 - s.pages.Notice(w, "remove", "Unable to find artifact.") 211 return 212 } 213 ··· 215 216 if user.Did != artifact.Did { 217 log.Println("user not authorized to delete artifact", err) 218 - s.pages.Notice(w, "remove", "Unauthorized deletion of artifact.") 219 return 220 } 221 ··· 226 }) 227 if err != nil { 228 log.Println("failed to get blob from pds", err) 229 - s.pages.Notice(w, "remove", "Failed to remove blob from PDS.") 230 return 231 } 232 233 - tx, err := s.db.BeginTx(r.Context(), nil) 234 if err != nil { 235 log.Println("failed to start tx") 236 - s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 237 return 238 } 239 defer tx.Rollback() ··· 245 ) 246 if err != nil { 247 log.Println("failed to remove artifact record from db", err) 248 - s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 249 return 250 } 251 252 err = tx.Commit() 253 if err != nil { 254 log.Println("failed to remove artifact record from db") 255 - s.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 256 return 257 } 258 259 w.Write([]byte{}) 260 } 261 262 - func (s *State) resolveTag(f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) { 263 tagParam, err := url.QueryUnescape(tagParam) 264 if err != nil { 265 return nil, err 266 } 267 268 - us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 269 if err != nil { 270 return nil, err 271 }
··· 1 + package repo 2 3 import ( 4 "fmt" ··· 23 ) 24 25 // TODO: proper statuses here on early exit 26 + func (rp *Repo) AttachArtifact(w http.ResponseWriter, r *http.Request) { 27 + user := rp.oauth.GetUser(r) 28 tagParam := chi.URLParam(r, "tag") 29 + f, err := rp.repoResolver.Resolve(r) 30 if err != nil { 31 log.Println("failed to get repo and knot", err) 32 + rp.pages.Notice(w, "upload", "failed to upload artifact, error in repo resolution") 33 return 34 } 35 36 + tag, err := rp.resolveTag(f, tagParam) 37 if err != nil { 38 log.Println("failed to resolve tag", err) 39 + rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution") 40 return 41 } 42 43 file, handler, err := r.FormFile("artifact") 44 if err != nil { 45 log.Println("failed to upload artifact", err) 46 + rp.pages.Notice(w, "upload", "failed to upload artifact") 47 return 48 } 49 defer file.Close() 50 51 + client, err := rp.oauth.AuthorizedClient(r) 52 if err != nil { 53 log.Println("failed to get authorized client", err) 54 + rp.pages.Notice(w, "upload", "failed to get authorized client") 55 return 56 } 57 58 uploadBlobResp, err := client.RepoUploadBlob(r.Context(), file) 59 if err != nil { 60 log.Println("failed to upload blob", err) 61 + rp.pages.Notice(w, "upload", "Failed to upload blob to your PDS. Try again later.") 62 return 63 } 64 ··· 83 }) 84 if err != nil { 85 log.Println("failed to create record", err) 86 + rp.pages.Notice(w, "upload", "Failed to create artifact record. Try again later.") 87 return 88 } 89 90 log.Println(putRecordResp.Uri) 91 92 + tx, err := rp.db.BeginTx(r.Context(), nil) 93 if err != nil { 94 log.Println("failed to start tx") 95 + rp.pages.Notice(w, "upload", "Failed to create artifact. Try again later.") 96 return 97 } 98 defer tx.Rollback() ··· 112 err = db.AddArtifact(tx, artifact) 113 if err != nil { 114 log.Println("failed to add artifact record to db", err) 115 + rp.pages.Notice(w, "upload", "Failed to create artifact. Try again later.") 116 return 117 } 118 119 err = tx.Commit() 120 if err != nil { 121 log.Println("failed to add artifact record to db") 122 + rp.pages.Notice(w, "upload", "Failed to create artifact. Try again later.") 123 return 124 } 125 126 + rp.pages.RepoArtifactFragment(w, pages.RepoArtifactParams{ 127 LoggedInUser: user, 128 RepoInfo: f.RepoInfo(user), 129 Artifact: artifact, ··· 131 } 132 133 // TODO: proper statuses here on early exit 134 + func (rp *Repo) DownloadArtifact(w http.ResponseWriter, r *http.Request) { 135 tagParam := chi.URLParam(r, "tag") 136 filename := chi.URLParam(r, "file") 137 + f, err := rp.repoResolver.Resolve(r) 138 if err != nil { 139 log.Println("failed to get repo and knot", err) 140 return 141 } 142 143 + tag, err := rp.resolveTag(f, tagParam) 144 if err != nil { 145 log.Println("failed to resolve tag", err) 146 + rp.pages.Notice(w, "upload", "failed to upload artifact, error in tag resolution") 147 return 148 } 149 150 + client, err := rp.oauth.AuthorizedClient(r) 151 if err != nil { 152 log.Println("failed to get authorized client", err) 153 return 154 } 155 156 artifacts, err := db.GetArtifact( 157 + rp.db, 158 db.FilterEq("repo_at", f.RepoAt), 159 db.FilterEq("tag", tag.Tag.Hash[:]), 160 db.FilterEq("name", filename), ··· 181 } 182 183 // TODO: proper statuses here on early exit 184 + func (rp *Repo) DeleteArtifact(w http.ResponseWriter, r *http.Request) { 185 + user := rp.oauth.GetUser(r) 186 tagParam := chi.URLParam(r, "tag") 187 filename := chi.URLParam(r, "file") 188 + f, err := rp.repoResolver.Resolve(r) 189 if err != nil { 190 log.Println("failed to get repo and knot", err) 191 return 192 } 193 194 + client, _ := rp.oauth.AuthorizedClient(r) 195 196 tag := plumbing.NewHash(tagParam) 197 198 artifacts, err := db.GetArtifact( 199 + rp.db, 200 db.FilterEq("repo_at", f.RepoAt), 201 db.FilterEq("tag", tag[:]), 202 db.FilterEq("name", filename), 203 ) 204 if err != nil { 205 log.Println("failed to get artifacts", err) 206 + rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 207 return 208 } 209 if len(artifacts) != 1 { 210 + rp.pages.Notice(w, "remove", "Unable to find artifact.") 211 return 212 } 213 ··· 215 216 if user.Did != artifact.Did { 217 log.Println("user not authorized to delete artifact", err) 218 + rp.pages.Notice(w, "remove", "Unauthorized deletion of artifact.") 219 return 220 } 221 ··· 226 }) 227 if err != nil { 228 log.Println("failed to get blob from pds", err) 229 + rp.pages.Notice(w, "remove", "Failed to remove blob from PDS.") 230 return 231 } 232 233 + tx, err := rp.db.BeginTx(r.Context(), nil) 234 if err != nil { 235 log.Println("failed to start tx") 236 + rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 237 return 238 } 239 defer tx.Rollback() ··· 245 ) 246 if err != nil { 247 log.Println("failed to remove artifact record from db", err) 248 + rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 249 return 250 } 251 252 err = tx.Commit() 253 if err != nil { 254 log.Println("failed to remove artifact record from db") 255 + rp.pages.Notice(w, "remove", "Failed to delete artifact. Try again later.") 256 return 257 } 258 259 w.Write([]byte{}) 260 } 261 262 + func (rp *Repo) resolveTag(f *reporesolver.ResolvedRepo, tagParam string) (*types.TagReference, error) { 263 tagParam, err := url.QueryUnescape(tagParam) 264 if err != nil { 265 return nil, err 266 } 267 268 + us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 269 if err != nil { 270 return nil, err 271 }
+295 -261
appview/state/repo.go appview/repo/repo.go
··· 1 - package state 2 3 import ( 4 "database/sql" ··· 19 "tangled.sh/tangled.sh/core/api/tangled" 20 "tangled.sh/tangled.sh/core/appview" 21 "tangled.sh/tangled.sh/core/appview/db" 22 "tangled.sh/tangled.sh/core/appview/oauth" 23 "tangled.sh/tangled.sh/core/appview/pages" 24 "tangled.sh/tangled.sh/core/appview/pages/markup" ··· 27 "tangled.sh/tangled.sh/core/appview/reporesolver" 28 "tangled.sh/tangled.sh/core/knotclient" 29 "tangled.sh/tangled.sh/core/patchutil" 30 "tangled.sh/tangled.sh/core/types" 31 32 "github.com/bluesky-social/indigo/atproto/data" ··· 39 lexutil "github.com/bluesky-social/indigo/lex/util" 40 ) 41 42 - func (s *State) RepoIndex(w http.ResponseWriter, r *http.Request) { 43 ref := chi.URLParam(r, "ref") 44 - f, err := s.repoResolver.Resolve(r) 45 if err != nil { 46 log.Println("failed to fully resolve repo", err) 47 return 48 } 49 50 - us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 51 if err != nil { 52 log.Printf("failed to create unsigned client for %s", f.Knot) 53 - s.pages.Error503(w) 54 return 55 } 56 57 result, err := us.Index(f.OwnerDid(), f.RepoName, ref) 58 if err != nil { 59 - s.pages.Error503(w) 60 log.Println("failed to reach knotserver", err) 61 return 62 } ··· 107 108 emails := uniqueEmails(commitsTrunc) 109 110 - user := s.oauth.GetUser(r) 111 repoInfo := f.RepoInfo(user) 112 113 - secret, err := db.GetRegistrationKey(s.db, f.Knot) 114 if err != nil { 115 log.Printf("failed to get registration key for %s: %s", f.Knot, err) 116 - s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 117 } 118 119 - signedClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 120 if err != nil { 121 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 122 return ··· 124 125 var forkInfo *types.ForkInfo 126 if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) { 127 - forkInfo, err = getForkInfo(repoInfo, s, f, user, signedClient) 128 if err != nil { 129 log.Printf("Failed to fetch fork information: %v", err) 130 return ··· 137 // non-fatal 138 } 139 140 - s.pages.RepoIndexPage(w, pages.RepoIndexParams{ 141 LoggedInUser: user, 142 RepoInfo: repoInfo, 143 TagMap: tagMap, ··· 146 TagsTrunc: tagsTrunc, 147 ForkInfo: forkInfo, 148 BranchesTrunc: branchesTrunc, 149 - EmailToDidOrHandle: EmailToDidOrHandle(s, emails), 150 Languages: repoLanguages, 151 }) 152 return ··· 154 155 func getForkInfo( 156 repoInfo repoinfo.RepoInfo, 157 - s *State, 158 f *reporesolver.ResolvedRepo, 159 user *oauth.User, 160 signedClient *knotclient.SignedClient, ··· 173 return &forkInfo, nil 174 } 175 176 - us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, s.config.Core.Dev) 177 if err != nil { 178 log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot) 179 return nil, err ··· 216 return &forkInfo, nil 217 } 218 219 - func (s *State) RepoLog(w http.ResponseWriter, r *http.Request) { 220 - f, err := s.repoResolver.Resolve(r) 221 if err != nil { 222 log.Println("failed to fully resolve repo", err) 223 return ··· 233 234 ref := chi.URLParam(r, "ref") 235 236 - us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 237 if err != nil { 238 log.Println("failed to create unsigned client", err) 239 return ··· 260 tagMap[hash] = append(tagMap[hash], tag.Name) 261 } 262 263 - user := s.oauth.GetUser(r) 264 - s.pages.RepoLog(w, pages.RepoLogParams{ 265 LoggedInUser: user, 266 TagMap: tagMap, 267 RepoInfo: f.RepoInfo(user), 268 RepoLogResponse: *repolog, 269 - EmailToDidOrHandle: EmailToDidOrHandle(s, uniqueEmails(repolog.Commits)), 270 }) 271 return 272 } 273 274 - func (s *State) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 275 - f, err := s.repoResolver.Resolve(r) 276 if err != nil { 277 log.Println("failed to get repo and knot", err) 278 w.WriteHeader(http.StatusBadRequest) 279 return 280 } 281 282 - user := s.oauth.GetUser(r) 283 - s.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 284 RepoInfo: f.RepoInfo(user), 285 }) 286 return 287 } 288 289 - func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) { 290 - f, err := s.repoResolver.Resolve(r) 291 if err != nil { 292 log.Println("failed to get repo and knot", err) 293 w.WriteHeader(http.StatusBadRequest) ··· 302 return 303 } 304 305 - user := s.oauth.GetUser(r) 306 307 switch r.Method { 308 case http.MethodGet: 309 - s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 310 RepoInfo: f.RepoInfo(user), 311 }) 312 return 313 case http.MethodPut: 314 - user := s.oauth.GetUser(r) 315 newDescription := r.FormValue("description") 316 - client, err := s.oauth.AuthorizedClient(r) 317 if err != nil { 318 log.Println("failed to get client") 319 - s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 320 return 321 } 322 323 // optimistic update 324 - err = db.UpdateDescription(s.db, string(repoAt), newDescription) 325 if err != nil { 326 log.Println("failed to perferom update-description query", err) 327 - s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 328 return 329 } 330 ··· 334 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey) 335 if err != nil { 336 // failed to get record 337 - s.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") 338 return 339 } 340 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ ··· 356 if err != nil { 357 log.Println("failed to perferom update-description query", err) 358 // failed to get record 359 - s.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.") 360 return 361 } 362 363 newRepoInfo := f.RepoInfo(user) 364 newRepoInfo.Description = newDescription 365 366 - s.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 367 RepoInfo: newRepoInfo, 368 }) 369 return 370 } 371 } 372 373 - func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) { 374 - f, err := s.repoResolver.Resolve(r) 375 if err != nil { 376 log.Println("failed to fully resolve repo", err) 377 return 378 } 379 ref := chi.URLParam(r, "ref") 380 protocol := "http" 381 - if !s.config.Core.Dev { 382 protocol = "https" 383 } 384 385 if !plumbing.IsHash(ref) { 386 - s.pages.Error404(w) 387 return 388 } 389 ··· 406 return 407 } 408 409 - user := s.oauth.GetUser(r) 410 - s.pages.RepoCommit(w, pages.RepoCommitParams{ 411 LoggedInUser: user, 412 RepoInfo: f.RepoInfo(user), 413 RepoCommitResponse: result, 414 - EmailToDidOrHandle: EmailToDidOrHandle(s, []string{result.Diff.Commit.Author.Email}), 415 }) 416 return 417 } 418 419 - func (s *State) RepoTree(w http.ResponseWriter, r *http.Request) { 420 - f, err := s.repoResolver.Resolve(r) 421 if err != nil { 422 log.Println("failed to fully resolve repo", err) 423 return ··· 426 ref := chi.URLParam(r, "ref") 427 treePath := chi.URLParam(r, "*") 428 protocol := "http" 429 - if !s.config.Core.Dev { 430 protocol = "https" 431 } 432 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) ··· 455 return 456 } 457 458 - user := s.oauth.GetUser(r) 459 460 var breadcrumbs [][]string 461 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) ··· 468 baseTreeLink := path.Join(f.OwnerSlashRepo(), "tree", ref, treePath) 469 baseBlobLink := path.Join(f.OwnerSlashRepo(), "blob", ref, treePath) 470 471 - s.pages.RepoTree(w, pages.RepoTreeParams{ 472 LoggedInUser: user, 473 BreadCrumbs: breadcrumbs, 474 BaseTreeLink: baseTreeLink, ··· 479 return 480 } 481 482 - func (s *State) RepoTags(w http.ResponseWriter, r *http.Request) { 483 - f, err := s.repoResolver.Resolve(r) 484 if err != nil { 485 log.Println("failed to get repo and knot", err) 486 return 487 } 488 489 - us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 490 if err != nil { 491 log.Println("failed to create unsigned client", err) 492 return ··· 498 return 499 } 500 501 - artifacts, err := db.GetArtifact(s.db, db.FilterEq("repo_at", f.RepoAt)) 502 if err != nil { 503 log.Println("failed grab artifacts", err) 504 return ··· 526 } 527 } 528 529 - user := s.oauth.GetUser(r) 530 - s.pages.RepoTags(w, pages.RepoTagsParams{ 531 LoggedInUser: user, 532 RepoInfo: f.RepoInfo(user), 533 RepoTagsResponse: *result, ··· 537 return 538 } 539 540 - func (s *State) RepoBranches(w http.ResponseWriter, r *http.Request) { 541 - f, err := s.repoResolver.Resolve(r) 542 if err != nil { 543 log.Println("failed to get repo and knot", err) 544 return 545 } 546 547 - us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 548 if err != nil { 549 log.Println("failed to create unsigned client", err) 550 return ··· 573 return strings.Compare(a.Name, b.Name) * -1 574 }) 575 576 - user := s.oauth.GetUser(r) 577 - s.pages.RepoBranches(w, pages.RepoBranchesParams{ 578 LoggedInUser: user, 579 RepoInfo: f.RepoInfo(user), 580 RepoBranchesResponse: *result, ··· 582 return 583 } 584 585 - func (s *State) RepoBlob(w http.ResponseWriter, r *http.Request) { 586 - f, err := s.repoResolver.Resolve(r) 587 if err != nil { 588 log.Println("failed to get repo and knot", err) 589 return ··· 592 ref := chi.URLParam(r, "ref") 593 filePath := chi.URLParam(r, "*") 594 protocol := "http" 595 - if !s.config.Core.Dev { 596 protocol = "https" 597 } 598 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) ··· 630 showRendered = r.URL.Query().Get("code") != "true" 631 } 632 633 - user := s.oauth.GetUser(r) 634 - s.pages.RepoBlob(w, pages.RepoBlobParams{ 635 LoggedInUser: user, 636 RepoInfo: f.RepoInfo(user), 637 RepoBlobResponse: result, ··· 642 return 643 } 644 645 - func (s *State) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 646 - f, err := s.repoResolver.Resolve(r) 647 if err != nil { 648 log.Println("failed to get repo and knot", err) 649 return ··· 653 filePath := chi.URLParam(r, "*") 654 655 protocol := "http" 656 - if !s.config.Core.Dev { 657 protocol = "https" 658 } 659 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) ··· 686 return 687 } 688 689 - func (s *State) AddCollaborator(w http.ResponseWriter, r *http.Request) { 690 - f, err := s.repoResolver.Resolve(r) 691 if err != nil { 692 log.Println("failed to get repo and knot", err) 693 return ··· 699 return 700 } 701 702 - collaboratorIdent, err := s.idResolver.ResolveIdent(r.Context(), collaborator) 703 if err != nil { 704 w.Write([]byte("failed to resolve collaborator did to a handle")) 705 return ··· 708 709 // TODO: create an atproto record for this 710 711 - secret, err := db.GetRegistrationKey(s.db, f.Knot) 712 if err != nil { 713 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 714 return 715 } 716 717 - ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 718 if err != nil { 719 log.Println("failed to create client to ", f.Knot) 720 return ··· 731 return 732 } 733 734 - tx, err := s.db.BeginTx(r.Context(), nil) 735 if err != nil { 736 log.Println("failed to start tx") 737 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) ··· 739 } 740 defer func() { 741 tx.Rollback() 742 - err = s.enforcer.E.LoadPolicy() 743 if err != nil { 744 log.Println("failed to rollback policies") 745 } 746 }() 747 748 - err = s.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 749 if err != nil { 750 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 751 return 752 } 753 754 - err = db.AddCollaborator(s.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 755 if err != nil { 756 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 757 return ··· 764 return 765 } 766 767 - err = s.enforcer.E.SavePolicy() 768 if err != nil { 769 log.Println("failed to update ACLs", err) 770 http.Error(w, err.Error(), http.StatusInternalServerError) ··· 775 776 } 777 778 - func (s *State) DeleteRepo(w http.ResponseWriter, r *http.Request) { 779 - user := s.oauth.GetUser(r) 780 781 - f, err := s.repoResolver.Resolve(r) 782 if err != nil { 783 log.Println("failed to get repo and knot", err) 784 return 785 } 786 787 // remove record from pds 788 - xrpcClient, err := s.oauth.AuthorizedClient(r) 789 if err != nil { 790 log.Println("failed to get authorized client", err) 791 return ··· 798 }) 799 if err != nil { 800 log.Printf("failed to delete record: %s", err) 801 - s.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 802 return 803 } 804 log.Println("removed repo record ", f.RepoAt.String()) 805 806 - secret, err := db.GetRegistrationKey(s.db, f.Knot) 807 if err != nil { 808 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 809 return 810 } 811 812 - ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 813 if err != nil { 814 log.Println("failed to create client to ", f.Knot) 815 return ··· 827 log.Println("removed repo from knot ", f.Knot) 828 } 829 830 - tx, err := s.db.BeginTx(r.Context(), nil) 831 if err != nil { 832 log.Println("failed to start tx") 833 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) ··· 835 } 836 defer func() { 837 tx.Rollback() 838 - err = s.enforcer.E.LoadPolicy() 839 if err != nil { 840 log.Println("failed to rollback policies") 841 } 842 }() 843 844 // remove collaborator RBAC 845 - repoCollaborators, err := s.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 846 if err != nil { 847 - s.pages.Notice(w, "settings-delete", "Failed to remove collaborators") 848 return 849 } 850 for _, c := range repoCollaborators { 851 did := c[0] 852 - s.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) 853 } 854 log.Println("removed collaborators") 855 856 // remove repo RBAC 857 - err = s.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) 858 if err != nil { 859 - s.pages.Notice(w, "settings-delete", "Failed to update RBAC rules") 860 return 861 } 862 863 // remove repo from db 864 err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) 865 if err != nil { 866 - s.pages.Notice(w, "settings-delete", "Failed to update appview") 867 return 868 } 869 log.Println("removed repo from db") ··· 875 return 876 } 877 878 - err = s.enforcer.E.SavePolicy() 879 if err != nil { 880 log.Println("failed to update ACLs", err) 881 http.Error(w, err.Error(), http.StatusInternalServerError) 882 return 883 } 884 885 - s.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid())) 886 } 887 888 - func (s *State) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 889 - f, err := s.repoResolver.Resolve(r) 890 if err != nil { 891 log.Println("failed to get repo and knot", err) 892 return ··· 898 return 899 } 900 901 - secret, err := db.GetRegistrationKey(s.db, f.Knot) 902 if err != nil { 903 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 904 return 905 } 906 907 - ksClient, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 908 if err != nil { 909 log.Println("failed to create client to ", f.Knot) 910 return ··· 917 } 918 919 if ksResp.StatusCode != http.StatusNoContent { 920 - s.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.") 921 return 922 } 923 924 w.Write([]byte(fmt.Sprint("default branch set to: ", branch))) 925 } 926 927 - func (s *State) RepoSettings(w http.ResponseWriter, r *http.Request) { 928 - f, err := s.repoResolver.Resolve(r) 929 if err != nil { 930 log.Println("failed to get repo and knot", err) 931 return ··· 934 switch r.Method { 935 case http.MethodGet: 936 // for now, this is just pubkeys 937 - user := s.oauth.GetUser(r) 938 repoCollaborators, err := f.Collaborators(r.Context()) 939 if err != nil { 940 log.Println("failed to get collaborators", err) ··· 942 943 isCollaboratorInviteAllowed := false 944 if user != nil { 945 - ok, err := s.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo()) 946 if err == nil && ok { 947 isCollaboratorInviteAllowed = true 948 } 949 } 950 951 - us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 952 if err != nil { 953 log.Println("failed to create unsigned client", err) 954 return ··· 960 return 961 } 962 963 - s.pages.RepoSettings(w, pages.RepoSettingsParams{ 964 LoggedInUser: user, 965 RepoInfo: f.RepoInfo(user), 966 Collaborators: repoCollaborators, ··· 970 } 971 } 972 973 - func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 974 - user := s.oauth.GetUser(r) 975 - f, err := s.repoResolver.Resolve(r) 976 if err != nil { 977 log.Println("failed to get repo and knot", err) 978 return ··· 986 return 987 } 988 989 - issue, comments, err := db.GetIssueWithComments(s.db, f.RepoAt, issueIdInt) 990 if err != nil { 991 log.Println("failed to get issue and comments", err) 992 - s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 993 return 994 } 995 996 - issueOwnerIdent, err := s.idResolver.ResolveIdent(r.Context(), issue.OwnerDid) 997 if err != nil { 998 log.Println("failed to resolve issue owner", err) 999 } ··· 1002 for i, comment := range comments { 1003 identsToResolve[i] = comment.OwnerDid 1004 } 1005 - resolvedIds := s.idResolver.ResolveIdents(r.Context(), identsToResolve) 1006 didHandleMap := make(map[string]string) 1007 for _, identity := range resolvedIds { 1008 if !identity.Handle.IsInvalidHandle() { ··· 1012 } 1013 } 1014 1015 - s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 1016 LoggedInUser: user, 1017 RepoInfo: f.RepoInfo(user), 1018 Issue: *issue, ··· 1024 1025 } 1026 1027 - func (s *State) CloseIssue(w http.ResponseWriter, r *http.Request) { 1028 - user := s.oauth.GetUser(r) 1029 - f, err := s.repoResolver.Resolve(r) 1030 if err != nil { 1031 log.Println("failed to get repo and knot", err) 1032 return ··· 1040 return 1041 } 1042 1043 - issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1044 if err != nil { 1045 log.Println("failed to get issue", err) 1046 - s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1047 return 1048 } 1049 ··· 1061 1062 closed := tangled.RepoIssueStateClosed 1063 1064 - client, err := s.oauth.AuthorizedClient(r) 1065 if err != nil { 1066 log.Println("failed to get authorized client", err) 1067 return ··· 1080 1081 if err != nil { 1082 log.Println("failed to update issue state", err) 1083 - s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1084 return 1085 } 1086 1087 - err = db.CloseIssue(s.db, f.RepoAt, issueIdInt) 1088 if err != nil { 1089 log.Println("failed to close issue", err) 1090 - s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1091 return 1092 } 1093 1094 - s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 1095 return 1096 } else { 1097 log.Println("user is not permitted to close issue") ··· 1100 } 1101 } 1102 1103 - func (s *State) ReopenIssue(w http.ResponseWriter, r *http.Request) { 1104 - user := s.oauth.GetUser(r) 1105 - f, err := s.repoResolver.Resolve(r) 1106 if err != nil { 1107 log.Println("failed to get repo and knot", err) 1108 return ··· 1116 return 1117 } 1118 1119 - issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1120 if err != nil { 1121 log.Println("failed to get issue", err) 1122 - s.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1123 return 1124 } 1125 ··· 1133 isIssueOwner := user.Did == issue.OwnerDid 1134 1135 if isCollaborator || isIssueOwner { 1136 - err := db.ReopenIssue(s.db, f.RepoAt, issueIdInt) 1137 if err != nil { 1138 log.Println("failed to reopen issue", err) 1139 - s.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 1140 return 1141 } 1142 - s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 1143 return 1144 } else { 1145 log.Println("user is not the owner of the repo") ··· 1148 } 1149 } 1150 1151 - func (s *State) NewIssueComment(w http.ResponseWriter, r *http.Request) { 1152 - user := s.oauth.GetUser(r) 1153 - f, err := s.repoResolver.Resolve(r) 1154 if err != nil { 1155 log.Println("failed to get repo and knot", err) 1156 return ··· 1168 case http.MethodPost: 1169 body := r.FormValue("body") 1170 if body == "" { 1171 - s.pages.Notice(w, "issue", "Body is required") 1172 return 1173 } 1174 1175 commentId := mathrand.IntN(1000000) 1176 rkey := appview.TID() 1177 1178 - err := db.NewIssueComment(s.db, &db.Comment{ 1179 OwnerDid: user.Did, 1180 RepoAt: f.RepoAt, 1181 Issue: issueIdInt, ··· 1185 }) 1186 if err != nil { 1187 log.Println("failed to create comment", err) 1188 - s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1189 return 1190 } 1191 1192 createdAt := time.Now().Format(time.RFC3339) 1193 commentIdInt64 := int64(commentId) 1194 ownerDid := user.Did 1195 - issueAt, err := db.GetIssueAt(s.db, f.RepoAt, issueIdInt) 1196 if err != nil { 1197 log.Println("failed to get issue at", err) 1198 - s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1199 return 1200 } 1201 1202 atUri := f.RepoAt.String() 1203 - client, err := s.oauth.AuthorizedClient(r) 1204 if err != nil { 1205 log.Println("failed to get authorized client", err) 1206 - s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1207 return 1208 } 1209 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ ··· 1223 }) 1224 if err != nil { 1225 log.Println("failed to create comment", err) 1226 - s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1227 return 1228 } 1229 1230 - s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId)) 1231 return 1232 } 1233 } 1234 1235 - func (s *State) IssueComment(w http.ResponseWriter, r *http.Request) { 1236 - user := s.oauth.GetUser(r) 1237 - f, err := s.repoResolver.Resolve(r) 1238 if err != nil { 1239 log.Println("failed to get repo and knot", err) 1240 return ··· 1256 return 1257 } 1258 1259 - issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1260 if err != nil { 1261 log.Println("failed to get issue", err) 1262 - s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1263 return 1264 } 1265 1266 - comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1267 if err != nil { 1268 http.Error(w, "bad comment id", http.StatusBadRequest) 1269 return 1270 } 1271 1272 - identity, err := s.idResolver.ResolveIdent(r.Context(), comment.OwnerDid) 1273 if err != nil { 1274 log.Println("failed to resolve did") 1275 return ··· 1282 didHandleMap[identity.DID.String()] = identity.DID.String() 1283 } 1284 1285 - s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1286 LoggedInUser: user, 1287 RepoInfo: f.RepoInfo(user), 1288 DidHandleMap: didHandleMap, ··· 1291 }) 1292 } 1293 1294 - func (s *State) EditIssueComment(w http.ResponseWriter, r *http.Request) { 1295 - user := s.oauth.GetUser(r) 1296 - f, err := s.repoResolver.Resolve(r) 1297 if err != nil { 1298 log.Println("failed to get repo and knot", err) 1299 return ··· 1315 return 1316 } 1317 1318 - issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1319 if err != nil { 1320 log.Println("failed to get issue", err) 1321 - s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1322 return 1323 } 1324 1325 - comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1326 if err != nil { 1327 http.Error(w, "bad comment id", http.StatusBadRequest) 1328 return ··· 1335 1336 switch r.Method { 1337 case http.MethodGet: 1338 - s.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{ 1339 LoggedInUser: user, 1340 RepoInfo: f.RepoInfo(user), 1341 Issue: issue, ··· 1344 case http.MethodPost: 1345 // extract form value 1346 newBody := r.FormValue("body") 1347 - client, err := s.oauth.AuthorizedClient(r) 1348 if err != nil { 1349 log.Println("failed to get authorized client", err) 1350 - s.pages.Notice(w, "issue-comment", "Failed to create comment.") 1351 return 1352 } 1353 rkey := comment.Rkey 1354 1355 // optimistic update 1356 edited := time.Now() 1357 - err = db.EditComment(s.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody) 1358 if err != nil { 1359 log.Println("failed to perferom update-description query", err) 1360 - s.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 1361 return 1362 } 1363 ··· 1368 if err != nil { 1369 // failed to get record 1370 log.Println(err, rkey) 1371 - s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 1372 return 1373 } 1374 value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json ··· 1408 comment.Edited = &edited 1409 1410 // return new comment body with htmx 1411 - s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1412 LoggedInUser: user, 1413 RepoInfo: f.RepoInfo(user), 1414 DidHandleMap: didHandleMap, ··· 1421 1422 } 1423 1424 - func (s *State) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 1425 - user := s.oauth.GetUser(r) 1426 - f, err := s.repoResolver.Resolve(r) 1427 if err != nil { 1428 log.Println("failed to get repo and knot", err) 1429 return ··· 1437 return 1438 } 1439 1440 - issue, err := db.GetIssue(s.db, f.RepoAt, issueIdInt) 1441 if err != nil { 1442 log.Println("failed to get issue", err) 1443 - s.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1444 return 1445 } 1446 ··· 1452 return 1453 } 1454 1455 - comment, err := db.GetComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1456 if err != nil { 1457 http.Error(w, "bad comment id", http.StatusBadRequest) 1458 return ··· 1470 1471 // optimistic deletion 1472 deleted := time.Now() 1473 - err = db.DeleteComment(s.db, f.RepoAt, issueIdInt, commentIdInt) 1474 if err != nil { 1475 log.Println("failed to delete comment") 1476 - s.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") 1477 return 1478 } 1479 1480 // delete from pds 1481 if comment.Rkey != "" { 1482 - client, err := s.oauth.AuthorizedClient(r) 1483 if err != nil { 1484 log.Println("failed to get authorized client", err) 1485 - s.pages.Notice(w, "issue-comment", "Failed to delete comment.") 1486 return 1487 } 1488 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ ··· 1503 comment.Deleted = &deleted 1504 1505 // htmx fragment of comment after deletion 1506 - s.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1507 LoggedInUser: user, 1508 RepoInfo: f.RepoInfo(user), 1509 DidHandleMap: didHandleMap, ··· 1513 return 1514 } 1515 1516 - func (s *State) RepoIssues(w http.ResponseWriter, r *http.Request) { 1517 params := r.URL.Query() 1518 state := params.Get("state") 1519 isOpen := true ··· 1532 page = pagination.FirstPage() 1533 } 1534 1535 - user := s.oauth.GetUser(r) 1536 - f, err := s.repoResolver.Resolve(r) 1537 if err != nil { 1538 log.Println("failed to get repo and knot", err) 1539 return 1540 } 1541 1542 - issues, err := db.GetIssues(s.db, f.RepoAt, isOpen, page) 1543 if err != nil { 1544 log.Println("failed to get issues", err) 1545 - s.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 1546 return 1547 } 1548 ··· 1550 for i, issue := range issues { 1551 identsToResolve[i] = issue.OwnerDid 1552 } 1553 - resolvedIds := s.idResolver.ResolveIdents(r.Context(), identsToResolve) 1554 didHandleMap := make(map[string]string) 1555 for _, identity := range resolvedIds { 1556 if !identity.Handle.IsInvalidHandle() { ··· 1560 } 1561 } 1562 1563 - s.pages.RepoIssues(w, pages.RepoIssuesParams{ 1564 - LoggedInUser: s.oauth.GetUser(r), 1565 RepoInfo: f.RepoInfo(user), 1566 Issues: issues, 1567 DidHandleMap: didHandleMap, ··· 1571 return 1572 } 1573 1574 - func (s *State) NewIssue(w http.ResponseWriter, r *http.Request) { 1575 - user := s.oauth.GetUser(r) 1576 1577 - f, err := s.repoResolver.Resolve(r) 1578 if err != nil { 1579 log.Println("failed to get repo and knot", err) 1580 return ··· 1582 1583 switch r.Method { 1584 case http.MethodGet: 1585 - s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 1586 LoggedInUser: user, 1587 RepoInfo: f.RepoInfo(user), 1588 }) ··· 1591 body := r.FormValue("body") 1592 1593 if title == "" || body == "" { 1594 - s.pages.Notice(w, "issues", "Title and body are required") 1595 return 1596 } 1597 1598 - tx, err := s.db.BeginTx(r.Context(), nil) 1599 if err != nil { 1600 - s.pages.Notice(w, "issues", "Failed to create issue, try again later") 1601 return 1602 } 1603 ··· 1609 }) 1610 if err != nil { 1611 log.Println("failed to create issue", err) 1612 - s.pages.Notice(w, "issues", "Failed to create issue.") 1613 return 1614 } 1615 1616 - issueId, err := db.GetIssueId(s.db, f.RepoAt) 1617 if err != nil { 1618 log.Println("failed to get issue id", err) 1619 - s.pages.Notice(w, "issues", "Failed to create issue.") 1620 return 1621 } 1622 1623 - client, err := s.oauth.AuthorizedClient(r) 1624 if err != nil { 1625 log.Println("failed to get authorized client", err) 1626 - s.pages.Notice(w, "issues", "Failed to create issue.") 1627 return 1628 } 1629 atUri := f.RepoAt.String() ··· 1643 }) 1644 if err != nil { 1645 log.Println("failed to create issue", err) 1646 - s.pages.Notice(w, "issues", "Failed to create issue.") 1647 return 1648 } 1649 1650 - err = db.SetIssueAt(s.db, f.RepoAt, issueId, resp.Uri) 1651 if err != nil { 1652 log.Println("failed to set issue at", err) 1653 - s.pages.Notice(w, "issues", "Failed to create issue.") 1654 return 1655 } 1656 1657 - if !s.config.Core.Dev { 1658 - err = s.posthog.Enqueue(posthog.Capture{ 1659 DistinctId: user.Did, 1660 Event: "new_issue", 1661 Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "issue_id": issueId}, ··· 1665 } 1666 } 1667 1668 - s.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) 1669 return 1670 } 1671 } 1672 1673 - func (s *State) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1674 - user := s.oauth.GetUser(r) 1675 - f, err := s.repoResolver.Resolve(r) 1676 if err != nil { 1677 log.Printf("failed to resolve source repo: %v", err) 1678 return ··· 1680 1681 switch r.Method { 1682 case http.MethodPost: 1683 - secret, err := db.GetRegistrationKey(s.db, f.Knot) 1684 if err != nil { 1685 - s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", f.Knot)) 1686 return 1687 } 1688 1689 - client, err := knotclient.NewSignedClient(f.Knot, secret, s.config.Core.Dev) 1690 if err != nil { 1691 - s.pages.Notice(w, "repo", "Failed to reach knot server.") 1692 return 1693 } 1694 1695 var uri string 1696 - if s.config.Core.Dev { 1697 uri = "http" 1698 } else { 1699 uri = "https" ··· 1703 1704 _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref) 1705 if err != nil { 1706 - s.pages.Notice(w, "repo", "Failed to sync repository fork.") 1707 return 1708 } 1709 1710 - s.pages.HxRefresh(w) 1711 return 1712 } 1713 } 1714 1715 - func (s *State) ForkRepo(w http.ResponseWriter, r *http.Request) { 1716 - user := s.oauth.GetUser(r) 1717 - f, err := s.repoResolver.Resolve(r) 1718 if err != nil { 1719 log.Printf("failed to resolve source repo: %v", err) 1720 return ··· 1722 1723 switch r.Method { 1724 case http.MethodGet: 1725 - user := s.oauth.GetUser(r) 1726 - knots, err := s.enforcer.GetDomainsForUser(user.Did) 1727 if err != nil { 1728 - s.pages.Notice(w, "repo", "Invalid user account.") 1729 return 1730 } 1731 1732 - s.pages.ForkRepo(w, pages.ForkRepoParams{ 1733 LoggedInUser: user, 1734 Knots: knots, 1735 RepoInfo: f.RepoInfo(user), ··· 1739 1740 knot := r.FormValue("knot") 1741 if knot == "" { 1742 - s.pages.Notice(w, "repo", "Invalid form submission&mdash;missing knot domain.") 1743 return 1744 } 1745 1746 - ok, err := s.enforcer.E.Enforce(user.Did, knot, knot, "repo:create") 1747 if err != nil || !ok { 1748 - s.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1749 return 1750 } 1751 ··· 1753 1754 // this check is *only* to see if the forked repo name already exists 1755 // in the user's account. 1756 - existingRepo, err := db.GetRepo(s.db, user.Did, f.RepoName) 1757 if err != nil { 1758 if errors.Is(err, sql.ErrNoRows) { 1759 // no existing repo with this name found, we can use the name as is 1760 } else { 1761 log.Println("error fetching existing repo from db", err) 1762 - s.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.") 1763 return 1764 } 1765 } else if existingRepo != nil { 1766 // repo with this name already exists, append random string 1767 forkName = fmt.Sprintf("%s-%s", forkName, randomString(3)) 1768 } 1769 - secret, err := db.GetRegistrationKey(s.db, knot) 1770 if err != nil { 1771 - s.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %s.", knot)) 1772 return 1773 } 1774 1775 - client, err := knotclient.NewSignedClient(knot, secret, s.config.Core.Dev) 1776 if err != nil { 1777 - s.pages.Notice(w, "repo", "Failed to reach knot server.") 1778 return 1779 } 1780 1781 var uri string 1782 - if s.config.Core.Dev { 1783 uri = "http" 1784 } else { 1785 uri = "https" ··· 1796 Source: sourceAt, 1797 } 1798 1799 - tx, err := s.db.BeginTx(r.Context(), nil) 1800 if err != nil { 1801 log.Println(err) 1802 - s.pages.Notice(w, "repo", "Failed to save repository information.") 1803 return 1804 } 1805 defer func() { 1806 tx.Rollback() 1807 - err = s.enforcer.E.LoadPolicy() 1808 if err != nil { 1809 log.Println("failed to rollback policies") 1810 } ··· 1812 1813 resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName) 1814 if err != nil { 1815 - s.pages.Notice(w, "repo", "Failed to create repository on knot server.") 1816 return 1817 } 1818 1819 switch resp.StatusCode { 1820 case http.StatusConflict: 1821 - s.pages.Notice(w, "repo", "A repository with that name already exists.") 1822 return 1823 case http.StatusInternalServerError: 1824 - s.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 1825 case http.StatusNoContent: 1826 // continue 1827 } 1828 1829 - xrpcClient, err := s.oauth.AuthorizedClient(r) 1830 if err != nil { 1831 log.Println("failed to get authorized client", err) 1832 - s.pages.Notice(w, "repo", "Failed to create repository.") 1833 return 1834 } 1835 ··· 1849 }) 1850 if err != nil { 1851 log.Printf("failed to create record: %s", err) 1852 - s.pages.Notice(w, "repo", "Failed to announce repository creation.") 1853 return 1854 } 1855 log.Println("created repo record: ", atresp.Uri) ··· 1858 err = db.AddRepo(tx, repo) 1859 if err != nil { 1860 log.Println(err) 1861 - s.pages.Notice(w, "repo", "Failed to save repository information.") 1862 return 1863 } 1864 1865 // acls 1866 p, _ := securejoin.SecureJoin(user.Did, forkName) 1867 - err = s.enforcer.AddRepo(user.Did, knot, p) 1868 if err != nil { 1869 log.Println(err) 1870 - s.pages.Notice(w, "repo", "Failed to set up repository permissions.") 1871 return 1872 } 1873 ··· 1878 return 1879 } 1880 1881 - err = s.enforcer.E.SavePolicy() 1882 if err != nil { 1883 log.Println("failed to update ACLs", err) 1884 http.Error(w, err.Error(), http.StatusInternalServerError) 1885 return 1886 } 1887 1888 - s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, forkName)) 1889 return 1890 } 1891 } 1892 1893 - func (s *State) RepoCompareNew(w http.ResponseWriter, r *http.Request) { 1894 - user := s.oauth.GetUser(r) 1895 - f, err := s.repoResolver.Resolve(r) 1896 if err != nil { 1897 log.Println("failed to get repo and knot", err) 1898 return 1899 } 1900 1901 - us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1902 if err != nil { 1903 log.Printf("failed to create unsigned client for %s", f.Knot) 1904 - s.pages.Error503(w) 1905 return 1906 } 1907 1908 result, err := us.Branches(f.OwnerDid(), f.RepoName) 1909 if err != nil { 1910 - s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1911 log.Println("failed to reach knotserver", err) 1912 return 1913 } ··· 1938 1939 tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1940 if err != nil { 1941 - s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1942 log.Println("failed to reach knotserver", err) 1943 return 1944 } 1945 1946 repoinfo := f.RepoInfo(user) 1947 1948 - s.pages.RepoCompareNew(w, pages.RepoCompareNewParams{ 1949 LoggedInUser: user, 1950 RepoInfo: repoinfo, 1951 Branches: branches, ··· 1955 }) 1956 } 1957 1958 - func (s *State) RepoCompare(w http.ResponseWriter, r *http.Request) { 1959 - user := s.oauth.GetUser(r) 1960 - f, err := s.repoResolver.Resolve(r) 1961 if err != nil { 1962 log.Println("failed to get repo and knot", err) 1963 return ··· 1979 1980 if base == "" || head == "" { 1981 log.Printf("invalid comparison") 1982 - s.pages.Error404(w) 1983 return 1984 } 1985 1986 - us, err := knotclient.NewUnsignedClient(f.Knot, s.config.Core.Dev) 1987 if err != nil { 1988 log.Printf("failed to create unsigned client for %s", f.Knot) 1989 - s.pages.Error503(w) 1990 return 1991 } 1992 1993 branches, err := us.Branches(f.OwnerDid(), f.RepoName) 1994 if err != nil { 1995 - s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1996 log.Println("failed to reach knotserver", err) 1997 return 1998 } 1999 2000 tags, err := us.Tags(f.OwnerDid(), f.RepoName) 2001 if err != nil { 2002 - s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2003 log.Println("failed to reach knotserver", err) 2004 return 2005 } 2006 2007 formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head) 2008 if err != nil { 2009 - s.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2010 log.Println("failed to compare", err) 2011 return 2012 } ··· 2014 2015 repoinfo := f.RepoInfo(user) 2016 2017 - s.pages.RepoCompare(w, pages.RepoCompareParams{ 2018 LoggedInUser: user, 2019 RepoInfo: repoinfo, 2020 Branches: branches.Branches,
··· 1 + package repo 2 3 import ( 4 "database/sql" ··· 19 "tangled.sh/tangled.sh/core/api/tangled" 20 "tangled.sh/tangled.sh/core/appview" 21 "tangled.sh/tangled.sh/core/appview/db" 22 + "tangled.sh/tangled.sh/core/appview/idresolver" 23 "tangled.sh/tangled.sh/core/appview/oauth" 24 "tangled.sh/tangled.sh/core/appview/pages" 25 "tangled.sh/tangled.sh/core/appview/pages/markup" ··· 28 "tangled.sh/tangled.sh/core/appview/reporesolver" 29 "tangled.sh/tangled.sh/core/knotclient" 30 "tangled.sh/tangled.sh/core/patchutil" 31 + "tangled.sh/tangled.sh/core/rbac" 32 "tangled.sh/tangled.sh/core/types" 33 34 "github.com/bluesky-social/indigo/atproto/data" ··· 41 lexutil "github.com/bluesky-social/indigo/lex/util" 42 ) 43 44 + type Repo struct { 45 + repoResolver *reporesolver.RepoResolver 46 + idResolver *idresolver.Resolver 47 + config *appview.Config 48 + oauth *oauth.OAuth 49 + pages *pages.Pages 50 + db *db.DB 51 + enforcer *rbac.Enforcer 52 + posthog posthog.Client 53 + } 54 + 55 + func New( 56 + oauth *oauth.OAuth, 57 + repoResolver *reporesolver.RepoResolver, 58 + pages *pages.Pages, 59 + idResolver *idresolver.Resolver, 60 + db *db.DB, 61 + config *appview.Config, 62 + posthog posthog.Client, 63 + enforcer *rbac.Enforcer, 64 + ) *Repo { 65 + return &Repo{oauth: oauth, 66 + repoResolver: repoResolver, 67 + pages: pages, 68 + idResolver: idResolver, 69 + config: config, 70 + db: db, 71 + posthog: posthog, 72 + enforcer: enforcer, 73 + } 74 + } 75 + 76 + func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { 77 ref := chi.URLParam(r, "ref") 78 + f, err := rp.repoResolver.Resolve(r) 79 if err != nil { 80 log.Println("failed to fully resolve repo", err) 81 return 82 } 83 84 + us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 85 if err != nil { 86 log.Printf("failed to create unsigned client for %s", f.Knot) 87 + rp.pages.Error503(w) 88 return 89 } 90 91 result, err := us.Index(f.OwnerDid(), f.RepoName, ref) 92 if err != nil { 93 + rp.pages.Error503(w) 94 log.Println("failed to reach knotserver", err) 95 return 96 } ··· 141 142 emails := uniqueEmails(commitsTrunc) 143 144 + user := rp.oauth.GetUser(r) 145 repoInfo := f.RepoInfo(user) 146 147 + secret, err := db.GetRegistrationKey(rp.db, f.Knot) 148 if err != nil { 149 log.Printf("failed to get registration key for %s: %s", f.Knot, err) 150 + rp.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") 151 } 152 153 + signedClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 154 if err != nil { 155 log.Printf("failed to create signed client for %s: %s", f.Knot, err) 156 return ··· 158 159 var forkInfo *types.ForkInfo 160 if user != nil && (repoInfo.Roles.IsOwner() || repoInfo.Roles.IsCollaborator()) { 161 + forkInfo, err = getForkInfo(repoInfo, rp, f, user, signedClient) 162 if err != nil { 163 log.Printf("Failed to fetch fork information: %v", err) 164 return ··· 171 // non-fatal 172 } 173 174 + rp.pages.RepoIndexPage(w, pages.RepoIndexParams{ 175 LoggedInUser: user, 176 RepoInfo: repoInfo, 177 TagMap: tagMap, ··· 180 TagsTrunc: tagsTrunc, 181 ForkInfo: forkInfo, 182 BranchesTrunc: branchesTrunc, 183 + EmailToDidOrHandle: EmailToDidOrHandle(rp, emails), 184 Languages: repoLanguages, 185 }) 186 return ··· 188 189 func getForkInfo( 190 repoInfo repoinfo.RepoInfo, 191 + rp *Repo, 192 f *reporesolver.ResolvedRepo, 193 user *oauth.User, 194 signedClient *knotclient.SignedClient, ··· 207 return &forkInfo, nil 208 } 209 210 + us, err := knotclient.NewUnsignedClient(repoInfo.Source.Knot, rp.config.Core.Dev) 211 if err != nil { 212 log.Printf("failed to create unsigned client for %s", repoInfo.Source.Knot) 213 return nil, err ··· 250 return &forkInfo, nil 251 } 252 253 + func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) { 254 + f, err := rp.repoResolver.Resolve(r) 255 if err != nil { 256 log.Println("failed to fully resolve repo", err) 257 return ··· 267 268 ref := chi.URLParam(r, "ref") 269 270 + us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 271 if err != nil { 272 log.Println("failed to create unsigned client", err) 273 return ··· 294 tagMap[hash] = append(tagMap[hash], tag.Name) 295 } 296 297 + user := rp.oauth.GetUser(r) 298 + rp.pages.RepoLog(w, pages.RepoLogParams{ 299 LoggedInUser: user, 300 TagMap: tagMap, 301 RepoInfo: f.RepoInfo(user), 302 RepoLogResponse: *repolog, 303 + EmailToDidOrHandle: EmailToDidOrHandle(rp, uniqueEmails(repolog.Commits)), 304 }) 305 return 306 } 307 308 + func (rp *Repo) RepoDescriptionEdit(w http.ResponseWriter, r *http.Request) { 309 + f, err := rp.repoResolver.Resolve(r) 310 if err != nil { 311 log.Println("failed to get repo and knot", err) 312 w.WriteHeader(http.StatusBadRequest) 313 return 314 } 315 316 + user := rp.oauth.GetUser(r) 317 + rp.pages.EditRepoDescriptionFragment(w, pages.RepoDescriptionParams{ 318 RepoInfo: f.RepoInfo(user), 319 }) 320 return 321 } 322 323 + func (rp *Repo) RepoDescription(w http.ResponseWriter, r *http.Request) { 324 + f, err := rp.repoResolver.Resolve(r) 325 if err != nil { 326 log.Println("failed to get repo and knot", err) 327 w.WriteHeader(http.StatusBadRequest) ··· 336 return 337 } 338 339 + user := rp.oauth.GetUser(r) 340 341 switch r.Method { 342 case http.MethodGet: 343 + rp.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 344 RepoInfo: f.RepoInfo(user), 345 }) 346 return 347 case http.MethodPut: 348 + user := rp.oauth.GetUser(r) 349 newDescription := r.FormValue("description") 350 + client, err := rp.oauth.AuthorizedClient(r) 351 if err != nil { 352 log.Println("failed to get client") 353 + rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 354 return 355 } 356 357 // optimistic update 358 + err = db.UpdateDescription(rp.db, string(repoAt), newDescription) 359 if err != nil { 360 log.Println("failed to perferom update-description query", err) 361 + rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 362 return 363 } 364 ··· 368 ex, err := client.RepoGetRecord(r.Context(), "", tangled.RepoNSID, user.Did, rkey) 369 if err != nil { 370 // failed to get record 371 + rp.pages.Notice(w, "repo-notice", "Failed to update description, no record found on PDS.") 372 return 373 } 374 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ ··· 390 if err != nil { 391 log.Println("failed to perferom update-description query", err) 392 // failed to get record 393 + rp.pages.Notice(w, "repo-notice", "Failed to update description, unable to save to PDS.") 394 return 395 } 396 397 newRepoInfo := f.RepoInfo(user) 398 newRepoInfo.Description = newDescription 399 400 + rp.pages.RepoDescriptionFragment(w, pages.RepoDescriptionParams{ 401 RepoInfo: newRepoInfo, 402 }) 403 return 404 } 405 } 406 407 + func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) { 408 + f, err := rp.repoResolver.Resolve(r) 409 if err != nil { 410 log.Println("failed to fully resolve repo", err) 411 return 412 } 413 ref := chi.URLParam(r, "ref") 414 protocol := "http" 415 + if !rp.config.Core.Dev { 416 protocol = "https" 417 } 418 419 if !plumbing.IsHash(ref) { 420 + rp.pages.Error404(w) 421 return 422 } 423 ··· 440 return 441 } 442 443 + user := rp.oauth.GetUser(r) 444 + rp.pages.RepoCommit(w, pages.RepoCommitParams{ 445 LoggedInUser: user, 446 RepoInfo: f.RepoInfo(user), 447 RepoCommitResponse: result, 448 + EmailToDidOrHandle: EmailToDidOrHandle(rp, []string{result.Diff.Commit.Author.Email}), 449 }) 450 return 451 } 452 453 + func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) { 454 + f, err := rp.repoResolver.Resolve(r) 455 if err != nil { 456 log.Println("failed to fully resolve repo", err) 457 return ··· 460 ref := chi.URLParam(r, "ref") 461 treePath := chi.URLParam(r, "*") 462 protocol := "http" 463 + if !rp.config.Core.Dev { 464 protocol = "https" 465 } 466 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/tree/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, treePath)) ··· 489 return 490 } 491 492 + user := rp.oauth.GetUser(r) 493 494 var breadcrumbs [][]string 495 breadcrumbs = append(breadcrumbs, []string{f.RepoName, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), ref)}) ··· 502 baseTreeLink := path.Join(f.OwnerSlashRepo(), "tree", ref, treePath) 503 baseBlobLink := path.Join(f.OwnerSlashRepo(), "blob", ref, treePath) 504 505 + rp.pages.RepoTree(w, pages.RepoTreeParams{ 506 LoggedInUser: user, 507 BreadCrumbs: breadcrumbs, 508 BaseTreeLink: baseTreeLink, ··· 513 return 514 } 515 516 + func (rp *Repo) RepoTags(w http.ResponseWriter, r *http.Request) { 517 + f, err := rp.repoResolver.Resolve(r) 518 if err != nil { 519 log.Println("failed to get repo and knot", err) 520 return 521 } 522 523 + us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 524 if err != nil { 525 log.Println("failed to create unsigned client", err) 526 return ··· 532 return 533 } 534 535 + artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt)) 536 if err != nil { 537 log.Println("failed grab artifacts", err) 538 return ··· 560 } 561 } 562 563 + user := rp.oauth.GetUser(r) 564 + rp.pages.RepoTags(w, pages.RepoTagsParams{ 565 LoggedInUser: user, 566 RepoInfo: f.RepoInfo(user), 567 RepoTagsResponse: *result, ··· 571 return 572 } 573 574 + func (rp *Repo) RepoBranches(w http.ResponseWriter, r *http.Request) { 575 + f, err := rp.repoResolver.Resolve(r) 576 if err != nil { 577 log.Println("failed to get repo and knot", err) 578 return 579 } 580 581 + us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 582 if err != nil { 583 log.Println("failed to create unsigned client", err) 584 return ··· 607 return strings.Compare(a.Name, b.Name) * -1 608 }) 609 610 + user := rp.oauth.GetUser(r) 611 + rp.pages.RepoBranches(w, pages.RepoBranchesParams{ 612 LoggedInUser: user, 613 RepoInfo: f.RepoInfo(user), 614 RepoBranchesResponse: *result, ··· 616 return 617 } 618 619 + func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) { 620 + f, err := rp.repoResolver.Resolve(r) 621 if err != nil { 622 log.Println("failed to get repo and knot", err) 623 return ··· 626 ref := chi.URLParam(r, "ref") 627 filePath := chi.URLParam(r, "*") 628 protocol := "http" 629 + if !rp.config.Core.Dev { 630 protocol = "https" 631 } 632 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) ··· 664 showRendered = r.URL.Query().Get("code") != "true" 665 } 666 667 + user := rp.oauth.GetUser(r) 668 + rp.pages.RepoBlob(w, pages.RepoBlobParams{ 669 LoggedInUser: user, 670 RepoInfo: f.RepoInfo(user), 671 RepoBlobResponse: result, ··· 676 return 677 } 678 679 + func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 680 + f, err := rp.repoResolver.Resolve(r) 681 if err != nil { 682 log.Println("failed to get repo and knot", err) 683 return ··· 687 filePath := chi.URLParam(r, "*") 688 689 protocol := "http" 690 + if !rp.config.Core.Dev { 691 protocol = "https" 692 } 693 resp, err := http.Get(fmt.Sprintf("%s://%s/%s/%s/blob/%s/%s", protocol, f.Knot, f.OwnerDid(), f.RepoName, ref, filePath)) ··· 720 return 721 } 722 723 + func (rp *Repo) AddCollaborator(w http.ResponseWriter, r *http.Request) { 724 + f, err := rp.repoResolver.Resolve(r) 725 if err != nil { 726 log.Println("failed to get repo and knot", err) 727 return ··· 733 return 734 } 735 736 + collaboratorIdent, err := rp.idResolver.ResolveIdent(r.Context(), collaborator) 737 if err != nil { 738 w.Write([]byte("failed to resolve collaborator did to a handle")) 739 return ··· 742 743 // TODO: create an atproto record for this 744 745 + secret, err := db.GetRegistrationKey(rp.db, f.Knot) 746 if err != nil { 747 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 748 return 749 } 750 751 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 752 if err != nil { 753 log.Println("failed to create client to ", f.Knot) 754 return ··· 765 return 766 } 767 768 + tx, err := rp.db.BeginTx(r.Context(), nil) 769 if err != nil { 770 log.Println("failed to start tx") 771 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) ··· 773 } 774 defer func() { 775 tx.Rollback() 776 + err = rp.enforcer.E.LoadPolicy() 777 if err != nil { 778 log.Println("failed to rollback policies") 779 } 780 }() 781 782 + err = rp.enforcer.AddCollaborator(collaboratorIdent.DID.String(), f.Knot, f.DidSlashRepo()) 783 if err != nil { 784 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 785 return 786 } 787 788 + err = db.AddCollaborator(rp.db, collaboratorIdent.DID.String(), f.OwnerDid(), f.RepoName, f.Knot) 789 if err != nil { 790 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) 791 return ··· 798 return 799 } 800 801 + err = rp.enforcer.E.SavePolicy() 802 if err != nil { 803 log.Println("failed to update ACLs", err) 804 http.Error(w, err.Error(), http.StatusInternalServerError) ··· 809 810 } 811 812 + func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 813 + user := rp.oauth.GetUser(r) 814 815 + f, err := rp.repoResolver.Resolve(r) 816 if err != nil { 817 log.Println("failed to get repo and knot", err) 818 return 819 } 820 821 // remove record from pds 822 + xrpcClient, err := rp.oauth.AuthorizedClient(r) 823 if err != nil { 824 log.Println("failed to get authorized client", err) 825 return ··· 832 }) 833 if err != nil { 834 log.Printf("failed to delete record: %s", err) 835 + rp.pages.Notice(w, "settings-delete", "Failed to delete repository from PDS.") 836 return 837 } 838 log.Println("removed repo record ", f.RepoAt.String()) 839 840 + secret, err := db.GetRegistrationKey(rp.db, f.Knot) 841 if err != nil { 842 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 843 return 844 } 845 846 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 847 if err != nil { 848 log.Println("failed to create client to ", f.Knot) 849 return ··· 861 log.Println("removed repo from knot ", f.Knot) 862 } 863 864 + tx, err := rp.db.BeginTx(r.Context(), nil) 865 if err != nil { 866 log.Println("failed to start tx") 867 w.Write([]byte(fmt.Sprint("failed to add collaborator: ", err))) ··· 869 } 870 defer func() { 871 tx.Rollback() 872 + err = rp.enforcer.E.LoadPolicy() 873 if err != nil { 874 log.Println("failed to rollback policies") 875 } 876 }() 877 878 // remove collaborator RBAC 879 + repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 880 if err != nil { 881 + rp.pages.Notice(w, "settings-delete", "Failed to remove collaborators") 882 return 883 } 884 for _, c := range repoCollaborators { 885 did := c[0] 886 + rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) 887 } 888 log.Println("removed collaborators") 889 890 // remove repo RBAC 891 + err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) 892 if err != nil { 893 + rp.pages.Notice(w, "settings-delete", "Failed to update RBAC rules") 894 return 895 } 896 897 // remove repo from db 898 err = db.RemoveRepo(tx, f.OwnerDid(), f.RepoName) 899 if err != nil { 900 + rp.pages.Notice(w, "settings-delete", "Failed to update appview") 901 return 902 } 903 log.Println("removed repo from db") ··· 909 return 910 } 911 912 + err = rp.enforcer.E.SavePolicy() 913 if err != nil { 914 log.Println("failed to update ACLs", err) 915 http.Error(w, err.Error(), http.StatusInternalServerError) 916 return 917 } 918 919 + rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid())) 920 } 921 922 + func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 923 + f, err := rp.repoResolver.Resolve(r) 924 if err != nil { 925 log.Println("failed to get repo and knot", err) 926 return ··· 932 return 933 } 934 935 + secret, err := db.GetRegistrationKey(rp.db, f.Knot) 936 if err != nil { 937 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 938 return 939 } 940 941 + ksClient, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 942 if err != nil { 943 log.Println("failed to create client to ", f.Knot) 944 return ··· 951 } 952 953 if ksResp.StatusCode != http.StatusNoContent { 954 + rp.pages.Notice(w, "repo-settings", "Failed to set default branch. Try again later.") 955 return 956 } 957 958 w.Write([]byte(fmt.Sprint("default branch set to: ", branch))) 959 } 960 961 + func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) { 962 + f, err := rp.repoResolver.Resolve(r) 963 if err != nil { 964 log.Println("failed to get repo and knot", err) 965 return ··· 968 switch r.Method { 969 case http.MethodGet: 970 // for now, this is just pubkeys 971 + user := rp.oauth.GetUser(r) 972 repoCollaborators, err := f.Collaborators(r.Context()) 973 if err != nil { 974 log.Println("failed to get collaborators", err) ··· 976 977 isCollaboratorInviteAllowed := false 978 if user != nil { 979 + ok, err := rp.enforcer.IsCollaboratorInviteAllowed(user.Did, f.Knot, f.DidSlashRepo()) 980 if err == nil && ok { 981 isCollaboratorInviteAllowed = true 982 } 983 } 984 985 + us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 986 if err != nil { 987 log.Println("failed to create unsigned client", err) 988 return ··· 994 return 995 } 996 997 + rp.pages.RepoSettings(w, pages.RepoSettingsParams{ 998 LoggedInUser: user, 999 RepoInfo: f.RepoInfo(user), 1000 Collaborators: repoCollaborators, ··· 1004 } 1005 } 1006 1007 + func (rp *Repo) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 1008 + user := rp.oauth.GetUser(r) 1009 + f, err := rp.repoResolver.Resolve(r) 1010 if err != nil { 1011 log.Println("failed to get repo and knot", err) 1012 return ··· 1020 return 1021 } 1022 1023 + issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt, issueIdInt) 1024 if err != nil { 1025 log.Println("failed to get issue and comments", err) 1026 + rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1027 return 1028 } 1029 1030 + issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid) 1031 if err != nil { 1032 log.Println("failed to resolve issue owner", err) 1033 } ··· 1036 for i, comment := range comments { 1037 identsToResolve[i] = comment.OwnerDid 1038 } 1039 + resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve) 1040 didHandleMap := make(map[string]string) 1041 for _, identity := range resolvedIds { 1042 if !identity.Handle.IsInvalidHandle() { ··· 1046 } 1047 } 1048 1049 + rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 1050 LoggedInUser: user, 1051 RepoInfo: f.RepoInfo(user), 1052 Issue: *issue, ··· 1058 1059 } 1060 1061 + func (rp *Repo) CloseIssue(w http.ResponseWriter, r *http.Request) { 1062 + user := rp.oauth.GetUser(r) 1063 + f, err := rp.repoResolver.Resolve(r) 1064 if err != nil { 1065 log.Println("failed to get repo and knot", err) 1066 return ··· 1074 return 1075 } 1076 1077 + issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 1078 if err != nil { 1079 log.Println("failed to get issue", err) 1080 + rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1081 return 1082 } 1083 ··· 1095 1096 closed := tangled.RepoIssueStateClosed 1097 1098 + client, err := rp.oauth.AuthorizedClient(r) 1099 if err != nil { 1100 log.Println("failed to get authorized client", err) 1101 return ··· 1114 1115 if err != nil { 1116 log.Println("failed to update issue state", err) 1117 + rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1118 return 1119 } 1120 1121 + err = db.CloseIssue(rp.db, f.RepoAt, issueIdInt) 1122 if err != nil { 1123 log.Println("failed to close issue", err) 1124 + rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1125 return 1126 } 1127 1128 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 1129 return 1130 } else { 1131 log.Println("user is not permitted to close issue") ··· 1134 } 1135 } 1136 1137 + func (rp *Repo) ReopenIssue(w http.ResponseWriter, r *http.Request) { 1138 + user := rp.oauth.GetUser(r) 1139 + f, err := rp.repoResolver.Resolve(r) 1140 if err != nil { 1141 log.Println("failed to get repo and knot", err) 1142 return ··· 1150 return 1151 } 1152 1153 + issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 1154 if err != nil { 1155 log.Println("failed to get issue", err) 1156 + rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.") 1157 return 1158 } 1159 ··· 1167 isIssueOwner := user.Did == issue.OwnerDid 1168 1169 if isCollaborator || isIssueOwner { 1170 + err := db.ReopenIssue(rp.db, f.RepoAt, issueIdInt) 1171 if err != nil { 1172 log.Println("failed to reopen issue", err) 1173 + rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.") 1174 return 1175 } 1176 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt)) 1177 return 1178 } else { 1179 log.Println("user is not the owner of the repo") ··· 1182 } 1183 } 1184 1185 + func (rp *Repo) NewIssueComment(w http.ResponseWriter, r *http.Request) { 1186 + user := rp.oauth.GetUser(r) 1187 + f, err := rp.repoResolver.Resolve(r) 1188 if err != nil { 1189 log.Println("failed to get repo and knot", err) 1190 return ··· 1202 case http.MethodPost: 1203 body := r.FormValue("body") 1204 if body == "" { 1205 + rp.pages.Notice(w, "issue", "Body is required") 1206 return 1207 } 1208 1209 commentId := mathrand.IntN(1000000) 1210 rkey := appview.TID() 1211 1212 + err := db.NewIssueComment(rp.db, &db.Comment{ 1213 OwnerDid: user.Did, 1214 RepoAt: f.RepoAt, 1215 Issue: issueIdInt, ··· 1219 }) 1220 if err != nil { 1221 log.Println("failed to create comment", err) 1222 + rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 1223 return 1224 } 1225 1226 createdAt := time.Now().Format(time.RFC3339) 1227 commentIdInt64 := int64(commentId) 1228 ownerDid := user.Did 1229 + issueAt, err := db.GetIssueAt(rp.db, f.RepoAt, issueIdInt) 1230 if err != nil { 1231 log.Println("failed to get issue at", err) 1232 + rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 1233 return 1234 } 1235 1236 atUri := f.RepoAt.String() 1237 + client, err := rp.oauth.AuthorizedClient(r) 1238 if err != nil { 1239 log.Println("failed to get authorized client", err) 1240 + rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 1241 return 1242 } 1243 _, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{ ··· 1257 }) 1258 if err != nil { 1259 log.Println("failed to create comment", err) 1260 + rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 1261 return 1262 } 1263 1264 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId)) 1265 return 1266 } 1267 } 1268 1269 + func (rp *Repo) IssueComment(w http.ResponseWriter, r *http.Request) { 1270 + user := rp.oauth.GetUser(r) 1271 + f, err := rp.repoResolver.Resolve(r) 1272 if err != nil { 1273 log.Println("failed to get repo and knot", err) 1274 return ··· 1290 return 1291 } 1292 1293 + issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 1294 if err != nil { 1295 log.Println("failed to get issue", err) 1296 + rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1297 return 1298 } 1299 1300 + comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 1301 if err != nil { 1302 http.Error(w, "bad comment id", http.StatusBadRequest) 1303 return 1304 } 1305 1306 + identity, err := rp.idResolver.ResolveIdent(r.Context(), comment.OwnerDid) 1307 if err != nil { 1308 log.Println("failed to resolve did") 1309 return ··· 1316 didHandleMap[identity.DID.String()] = identity.DID.String() 1317 } 1318 1319 + rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1320 LoggedInUser: user, 1321 RepoInfo: f.RepoInfo(user), 1322 DidHandleMap: didHandleMap, ··· 1325 }) 1326 } 1327 1328 + func (rp *Repo) EditIssueComment(w http.ResponseWriter, r *http.Request) { 1329 + user := rp.oauth.GetUser(r) 1330 + f, err := rp.repoResolver.Resolve(r) 1331 if err != nil { 1332 log.Println("failed to get repo and knot", err) 1333 return ··· 1349 return 1350 } 1351 1352 + issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 1353 if err != nil { 1354 log.Println("failed to get issue", err) 1355 + rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1356 return 1357 } 1358 1359 + comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 1360 if err != nil { 1361 http.Error(w, "bad comment id", http.StatusBadRequest) 1362 return ··· 1369 1370 switch r.Method { 1371 case http.MethodGet: 1372 + rp.pages.EditIssueCommentFragment(w, pages.EditIssueCommentParams{ 1373 LoggedInUser: user, 1374 RepoInfo: f.RepoInfo(user), 1375 Issue: issue, ··· 1378 case http.MethodPost: 1379 // extract form value 1380 newBody := r.FormValue("body") 1381 + client, err := rp.oauth.AuthorizedClient(r) 1382 if err != nil { 1383 log.Println("failed to get authorized client", err) 1384 + rp.pages.Notice(w, "issue-comment", "Failed to create comment.") 1385 return 1386 } 1387 rkey := comment.Rkey 1388 1389 // optimistic update 1390 edited := time.Now() 1391 + err = db.EditComment(rp.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody) 1392 if err != nil { 1393 log.Println("failed to perferom update-description query", err) 1394 + rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.") 1395 return 1396 } 1397 ··· 1402 if err != nil { 1403 // failed to get record 1404 log.Println(err, rkey) 1405 + rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "Failed to update description, no record found on PDS.") 1406 return 1407 } 1408 value, _ := ex.Value.MarshalJSON() // we just did get record; it is valid json ··· 1442 comment.Edited = &edited 1443 1444 // return new comment body with htmx 1445 + rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1446 LoggedInUser: user, 1447 RepoInfo: f.RepoInfo(user), 1448 DidHandleMap: didHandleMap, ··· 1455 1456 } 1457 1458 + func (rp *Repo) DeleteIssueComment(w http.ResponseWriter, r *http.Request) { 1459 + user := rp.oauth.GetUser(r) 1460 + f, err := rp.repoResolver.Resolve(r) 1461 if err != nil { 1462 log.Println("failed to get repo and knot", err) 1463 return ··· 1471 return 1472 } 1473 1474 + issue, err := db.GetIssue(rp.db, f.RepoAt, issueIdInt) 1475 if err != nil { 1476 log.Println("failed to get issue", err) 1477 + rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.") 1478 return 1479 } 1480 ··· 1486 return 1487 } 1488 1489 + comment, err := db.GetComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 1490 if err != nil { 1491 http.Error(w, "bad comment id", http.StatusBadRequest) 1492 return ··· 1504 1505 // optimistic deletion 1506 deleted := time.Now() 1507 + err = db.DeleteComment(rp.db, f.RepoAt, issueIdInt, commentIdInt) 1508 if err != nil { 1509 log.Println("failed to delete comment") 1510 + rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment") 1511 return 1512 } 1513 1514 // delete from pds 1515 if comment.Rkey != "" { 1516 + client, err := rp.oauth.AuthorizedClient(r) 1517 if err != nil { 1518 log.Println("failed to get authorized client", err) 1519 + rp.pages.Notice(w, "issue-comment", "Failed to delete comment.") 1520 return 1521 } 1522 _, err = client.RepoDeleteRecord(r.Context(), &comatproto.RepoDeleteRecord_Input{ ··· 1537 comment.Deleted = &deleted 1538 1539 // htmx fragment of comment after deletion 1540 + rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{ 1541 LoggedInUser: user, 1542 RepoInfo: f.RepoInfo(user), 1543 DidHandleMap: didHandleMap, ··· 1547 return 1548 } 1549 1550 + func (rp *Repo) RepoIssues(w http.ResponseWriter, r *http.Request) { 1551 params := r.URL.Query() 1552 state := params.Get("state") 1553 isOpen := true ··· 1566 page = pagination.FirstPage() 1567 } 1568 1569 + user := rp.oauth.GetUser(r) 1570 + f, err := rp.repoResolver.Resolve(r) 1571 if err != nil { 1572 log.Println("failed to get repo and knot", err) 1573 return 1574 } 1575 1576 + issues, err := db.GetIssues(rp.db, f.RepoAt, isOpen, page) 1577 if err != nil { 1578 log.Println("failed to get issues", err) 1579 + rp.pages.Notice(w, "issues", "Failed to load issues. Try again later.") 1580 return 1581 } 1582 ··· 1584 for i, issue := range issues { 1585 identsToResolve[i] = issue.OwnerDid 1586 } 1587 + resolvedIds := rp.idResolver.ResolveIdents(r.Context(), identsToResolve) 1588 didHandleMap := make(map[string]string) 1589 for _, identity := range resolvedIds { 1590 if !identity.Handle.IsInvalidHandle() { ··· 1594 } 1595 } 1596 1597 + rp.pages.RepoIssues(w, pages.RepoIssuesParams{ 1598 + LoggedInUser: rp.oauth.GetUser(r), 1599 RepoInfo: f.RepoInfo(user), 1600 Issues: issues, 1601 DidHandleMap: didHandleMap, ··· 1605 return 1606 } 1607 1608 + func (rp *Repo) NewIssue(w http.ResponseWriter, r *http.Request) { 1609 + user := rp.oauth.GetUser(r) 1610 1611 + f, err := rp.repoResolver.Resolve(r) 1612 if err != nil { 1613 log.Println("failed to get repo and knot", err) 1614 return ··· 1616 1617 switch r.Method { 1618 case http.MethodGet: 1619 + rp.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 1620 LoggedInUser: user, 1621 RepoInfo: f.RepoInfo(user), 1622 }) ··· 1625 body := r.FormValue("body") 1626 1627 if title == "" || body == "" { 1628 + rp.pages.Notice(w, "issues", "Title and body are required") 1629 return 1630 } 1631 1632 + tx, err := rp.db.BeginTx(r.Context(), nil) 1633 if err != nil { 1634 + rp.pages.Notice(w, "issues", "Failed to create issue, try again later") 1635 return 1636 } 1637 ··· 1643 }) 1644 if err != nil { 1645 log.Println("failed to create issue", err) 1646 + rp.pages.Notice(w, "issues", "Failed to create issue.") 1647 return 1648 } 1649 1650 + issueId, err := db.GetIssueId(rp.db, f.RepoAt) 1651 if err != nil { 1652 log.Println("failed to get issue id", err) 1653 + rp.pages.Notice(w, "issues", "Failed to create issue.") 1654 return 1655 } 1656 1657 + client, err := rp.oauth.AuthorizedClient(r) 1658 if err != nil { 1659 log.Println("failed to get authorized client", err) 1660 + rp.pages.Notice(w, "issues", "Failed to create issue.") 1661 return 1662 } 1663 atUri := f.RepoAt.String() ··· 1677 }) 1678 if err != nil { 1679 log.Println("failed to create issue", err) 1680 + rp.pages.Notice(w, "issues", "Failed to create issue.") 1681 return 1682 } 1683 1684 + err = db.SetIssueAt(rp.db, f.RepoAt, issueId, resp.Uri) 1685 if err != nil { 1686 log.Println("failed to set issue at", err) 1687 + rp.pages.Notice(w, "issues", "Failed to create issue.") 1688 return 1689 } 1690 1691 + if !rp.config.Core.Dev { 1692 + err = rp.posthog.Enqueue(posthog.Capture{ 1693 DistinctId: user.Did, 1694 Event: "new_issue", 1695 Properties: posthog.Properties{"repo_at": f.RepoAt.String(), "issue_id": issueId}, ··· 1699 } 1700 } 1701 1702 + rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueId)) 1703 return 1704 } 1705 } 1706 1707 + func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { 1708 + user := rp.oauth.GetUser(r) 1709 + f, err := rp.repoResolver.Resolve(r) 1710 if err != nil { 1711 log.Printf("failed to resolve source repo: %v", err) 1712 return ··· 1714 1715 switch r.Method { 1716 case http.MethodPost: 1717 + secret, err := db.GetRegistrationKey(rp.db, f.Knot) 1718 if err != nil { 1719 + rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", f.Knot)) 1720 return 1721 } 1722 1723 + client, err := knotclient.NewSignedClient(f.Knot, secret, rp.config.Core.Dev) 1724 if err != nil { 1725 + rp.pages.Notice(w, "repo", "Failed to reach knot server.") 1726 return 1727 } 1728 1729 var uri string 1730 + if rp.config.Core.Dev { 1731 uri = "http" 1732 } else { 1733 uri = "https" ··· 1737 1738 _, err = client.SyncRepoFork(user.Did, forkSourceUrl, forkName, f.Ref) 1739 if err != nil { 1740 + rp.pages.Notice(w, "repo", "Failed to sync repository fork.") 1741 return 1742 } 1743 1744 + rp.pages.HxRefresh(w) 1745 return 1746 } 1747 } 1748 1749 + func (rp *Repo) ForkRepo(w http.ResponseWriter, r *http.Request) { 1750 + user := rp.oauth.GetUser(r) 1751 + f, err := rp.repoResolver.Resolve(r) 1752 if err != nil { 1753 log.Printf("failed to resolve source repo: %v", err) 1754 return ··· 1756 1757 switch r.Method { 1758 case http.MethodGet: 1759 + user := rp.oauth.GetUser(r) 1760 + knots, err := rp.enforcer.GetDomainsForUser(user.Did) 1761 if err != nil { 1762 + rp.pages.Notice(w, "repo", "Invalid user account.") 1763 return 1764 } 1765 1766 + rp.pages.ForkRepo(w, pages.ForkRepoParams{ 1767 LoggedInUser: user, 1768 Knots: knots, 1769 RepoInfo: f.RepoInfo(user), ··· 1773 1774 knot := r.FormValue("knot") 1775 if knot == "" { 1776 + rp.pages.Notice(w, "repo", "Invalid form submission&mdash;missing knot domain.") 1777 return 1778 } 1779 1780 + ok, err := rp.enforcer.E.Enforce(user.Did, knot, knot, "repo:create") 1781 if err != nil || !ok { 1782 + rp.pages.Notice(w, "repo", "You do not have permission to create a repo in this knot.") 1783 return 1784 } 1785 ··· 1787 1788 // this check is *only* to see if the forked repo name already exists 1789 // in the user's account. 1790 + existingRepo, err := db.GetRepo(rp.db, user.Did, f.RepoName) 1791 if err != nil { 1792 if errors.Is(err, sql.ErrNoRows) { 1793 // no existing repo with this name found, we can use the name as is 1794 } else { 1795 log.Println("error fetching existing repo from db", err) 1796 + rp.pages.Notice(w, "repo", "Failed to fork this repository. Try again later.") 1797 return 1798 } 1799 } else if existingRepo != nil { 1800 // repo with this name already exists, append random string 1801 forkName = fmt.Sprintf("%s-%s", forkName, randomString(3)) 1802 } 1803 + secret, err := db.GetRegistrationKey(rp.db, knot) 1804 if err != nil { 1805 + rp.pages.Notice(w, "repo", fmt.Sprintf("No registration key found for knot %rp.", knot)) 1806 return 1807 } 1808 1809 + client, err := knotclient.NewSignedClient(knot, secret, rp.config.Core.Dev) 1810 if err != nil { 1811 + rp.pages.Notice(w, "repo", "Failed to reach knot server.") 1812 return 1813 } 1814 1815 var uri string 1816 + if rp.config.Core.Dev { 1817 uri = "http" 1818 } else { 1819 uri = "https" ··· 1830 Source: sourceAt, 1831 } 1832 1833 + tx, err := rp.db.BeginTx(r.Context(), nil) 1834 if err != nil { 1835 log.Println(err) 1836 + rp.pages.Notice(w, "repo", "Failed to save repository information.") 1837 return 1838 } 1839 defer func() { 1840 tx.Rollback() 1841 + err = rp.enforcer.E.LoadPolicy() 1842 if err != nil { 1843 log.Println("failed to rollback policies") 1844 } ··· 1846 1847 resp, err := client.ForkRepo(user.Did, forkSourceUrl, forkName) 1848 if err != nil { 1849 + rp.pages.Notice(w, "repo", "Failed to create repository on knot server.") 1850 return 1851 } 1852 1853 switch resp.StatusCode { 1854 case http.StatusConflict: 1855 + rp.pages.Notice(w, "repo", "A repository with that name already exists.") 1856 return 1857 case http.StatusInternalServerError: 1858 + rp.pages.Notice(w, "repo", "Failed to create repository on knot. Try again later.") 1859 case http.StatusNoContent: 1860 // continue 1861 } 1862 1863 + xrpcClient, err := rp.oauth.AuthorizedClient(r) 1864 if err != nil { 1865 log.Println("failed to get authorized client", err) 1866 + rp.pages.Notice(w, "repo", "Failed to create repository.") 1867 return 1868 } 1869 ··· 1883 }) 1884 if err != nil { 1885 log.Printf("failed to create record: %s", err) 1886 + rp.pages.Notice(w, "repo", "Failed to announce repository creation.") 1887 return 1888 } 1889 log.Println("created repo record: ", atresp.Uri) ··· 1892 err = db.AddRepo(tx, repo) 1893 if err != nil { 1894 log.Println(err) 1895 + rp.pages.Notice(w, "repo", "Failed to save repository information.") 1896 return 1897 } 1898 1899 // acls 1900 p, _ := securejoin.SecureJoin(user.Did, forkName) 1901 + err = rp.enforcer.AddRepo(user.Did, knot, p) 1902 if err != nil { 1903 log.Println(err) 1904 + rp.pages.Notice(w, "repo", "Failed to set up repository permissions.") 1905 return 1906 } 1907 ··· 1912 return 1913 } 1914 1915 + err = rp.enforcer.E.SavePolicy() 1916 if err != nil { 1917 log.Println("failed to update ACLs", err) 1918 http.Error(w, err.Error(), http.StatusInternalServerError) 1919 return 1920 } 1921 1922 + rp.pages.HxLocation(w, fmt.Sprintf("/@%s/%s", user.Handle, forkName)) 1923 return 1924 } 1925 } 1926 1927 + func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) { 1928 + user := rp.oauth.GetUser(r) 1929 + f, err := rp.repoResolver.Resolve(r) 1930 if err != nil { 1931 log.Println("failed to get repo and knot", err) 1932 return 1933 } 1934 1935 + us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 1936 if err != nil { 1937 log.Printf("failed to create unsigned client for %s", f.Knot) 1938 + rp.pages.Error503(w) 1939 return 1940 } 1941 1942 result, err := us.Branches(f.OwnerDid(), f.RepoName) 1943 if err != nil { 1944 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1945 log.Println("failed to reach knotserver", err) 1946 return 1947 } ··· 1972 1973 tags, err := us.Tags(f.OwnerDid(), f.RepoName) 1974 if err != nil { 1975 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 1976 log.Println("failed to reach knotserver", err) 1977 return 1978 } 1979 1980 repoinfo := f.RepoInfo(user) 1981 1982 + rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{ 1983 LoggedInUser: user, 1984 RepoInfo: repoinfo, 1985 Branches: branches, ··· 1989 }) 1990 } 1991 1992 + func (rp *Repo) RepoCompare(w http.ResponseWriter, r *http.Request) { 1993 + user := rp.oauth.GetUser(r) 1994 + f, err := rp.repoResolver.Resolve(r) 1995 if err != nil { 1996 log.Println("failed to get repo and knot", err) 1997 return ··· 2013 2014 if base == "" || head == "" { 2015 log.Printf("invalid comparison") 2016 + rp.pages.Error404(w) 2017 return 2018 } 2019 2020 + us, err := knotclient.NewUnsignedClient(f.Knot, rp.config.Core.Dev) 2021 if err != nil { 2022 log.Printf("failed to create unsigned client for %s", f.Knot) 2023 + rp.pages.Error503(w) 2024 return 2025 } 2026 2027 branches, err := us.Branches(f.OwnerDid(), f.RepoName) 2028 if err != nil { 2029 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2030 log.Println("failed to reach knotserver", err) 2031 return 2032 } 2033 2034 tags, err := us.Tags(f.OwnerDid(), f.RepoName) 2035 if err != nil { 2036 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2037 log.Println("failed to reach knotserver", err) 2038 return 2039 } 2040 2041 formatPatch, err := us.Compare(f.OwnerDid(), f.RepoName, base, head) 2042 if err != nil { 2043 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2044 log.Println("failed to compare", err) 2045 return 2046 } ··· 2048 2049 repoinfo := f.RepoInfo(user) 2050 2051 + rp.pages.RepoCompare(w, pages.RepoCompareParams{ 2052 LoggedInUser: user, 2053 RepoInfo: repoinfo, 2054 Branches: branches.Branches,
+4 -4
appview/state/repo_util.go appview/repo/repo_util.go
··· 1 - package state 2 3 import ( 4 "context" ··· 56 return 57 } 58 59 - func EmailToDidOrHandle(s *State, emails []string) map[string]string { 60 - emailToDid, err := db.GetEmailToDid(s.db, emails, true) // only get verified emails for mapping 61 if err != nil { 62 log.Printf("error fetching dids for emails: %v", err) 63 return nil ··· 67 for _, v := range emailToDid { 68 dids = append(dids, v) 69 } 70 - resolvedIdents := s.idResolver.ResolveIdents(context.Background(), dids) 71 72 didHandleMap := make(map[string]string) 73 for _, identity := range resolvedIdents {
··· 1 + package repo 2 3 import ( 4 "context" ··· 56 return 57 } 58 59 + func EmailToDidOrHandle(r *Repo, emails []string) map[string]string { 60 + emailToDid, err := db.GetEmailToDid(r.db, emails, true) // only get verified emails for mapping 61 if err != nil { 62 log.Printf("error fetching dids for emails: %v", err) 63 return nil ··· 67 for _, v := range emailToDid { 68 dids = append(dids, v) 69 } 70 + resolvedIdents := r.idResolver.ResolveIdents(context.Background(), dids) 71 72 didHandleMap := make(map[string]string) 73 for _, identity := range resolvedIdents {
+7 -85
appview/state/router.go
··· 9 "tangled.sh/tangled.sh/core/appview/middleware" 10 oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler" 11 "tangled.sh/tangled.sh/core/appview/pulls" 12 "tangled.sh/tangled.sh/core/appview/settings" 13 "tangled.sh/tangled.sh/core/appview/state/userutil" 14 ) ··· 69 r.With(mw.ResolveRepo()).Route("/{repo}", func(r chi.Router) { 70 r.Use(mw.GoImport()) 71 72 - r.Get("/", s.RepoIndex) 73 - r.Get("/commits/{ref}", s.RepoLog) 74 - r.Route("/tree/{ref}", func(r chi.Router) { 75 - r.Get("/", s.RepoIndex) 76 - r.Get("/*", s.RepoTree) 77 - }) 78 - r.Get("/commit/{ref}", s.RepoCommit) 79 - r.Get("/branches", s.RepoBranches) 80 - r.Route("/tags", func(r chi.Router) { 81 - r.Get("/", s.RepoTags) 82 - r.Route("/{tag}", func(r chi.Router) { 83 - r.Use(middleware.AuthMiddleware(s.oauth)) 84 - // require auth to download for now 85 - r.Get("/download/{file}", s.DownloadArtifact) 86 - 87 - // require repo:push to upload or delete artifacts 88 - // 89 - // additionally: only the uploader can truly delete an artifact 90 - // (record+blob will live on their pds) 91 - r.Group(func(r chi.Router) { 92 - r.With(mw.RepoPermissionMiddleware("repo:push")) 93 - r.Post("/upload", s.AttachArtifact) 94 - r.Delete("/{file}", s.DeleteArtifact) 95 - }) 96 - }) 97 - }) 98 - r.Get("/blob/{ref}/*", s.RepoBlob) 99 - r.Get("/raw/{ref}/*", s.RepoBlobRaw) 100 - 101 - r.Route("/issues", func(r chi.Router) { 102 - r.With(middleware.Paginate).Get("/", s.RepoIssues) 103 - r.Get("/{issue}", s.RepoSingleIssue) 104 - 105 - r.Group(func(r chi.Router) { 106 - r.Use(middleware.AuthMiddleware(s.oauth)) 107 - r.Get("/new", s.NewIssue) 108 - r.Post("/new", s.NewIssue) 109 - r.Post("/{issue}/comment", s.NewIssueComment) 110 - r.Route("/{issue}/comment/{comment_id}/", func(r chi.Router) { 111 - r.Get("/", s.IssueComment) 112 - r.Delete("/", s.DeleteIssueComment) 113 - r.Get("/edit", s.EditIssueComment) 114 - r.Post("/edit", s.EditIssueComment) 115 - }) 116 - r.Post("/{issue}/close", s.CloseIssue) 117 - r.Post("/{issue}/reopen", s.ReopenIssue) 118 - }) 119 - }) 120 - 121 - r.Route("/fork", func(r chi.Router) { 122 - r.Use(middleware.AuthMiddleware(s.oauth)) 123 - r.Get("/", s.ForkRepo) 124 - r.Post("/", s.ForkRepo) 125 - r.With(mw.RepoPermissionMiddleware("repo:owner")).Route("/sync", func(r chi.Router) { 126 - r.Post("/", s.SyncRepoFork) 127 - }) 128 - }) 129 - 130 - r.Route("/compare", func(r chi.Router) { 131 - r.Get("/", s.RepoCompareNew) // start an new comparison 132 - 133 - // we have to wildcard here since we want to support GitHub's compare syntax 134 - // /compare/{ref1}...{ref2} 135 - // for example: 136 - // /compare/master...some/feature 137 - // /compare/master...example.com:another/feature <- this is a fork 138 - r.Get("/{base}/{head}", s.RepoCompare) 139 - r.Get("/*", s.RepoCompare) 140 - }) 141 142 r.Mount("/pulls", s.PullsRouter(mw)) 143 ··· 146 r.Post("/git-upload-pack", s.UploadPack) 147 r.Post("/git-receive-pack", s.ReceivePack) 148 149 - // settings routes, needs auth 150 - r.Group(func(r chi.Router) { 151 - r.Use(middleware.AuthMiddleware(s.oauth)) 152 - // repo description can only be edited by owner 153 - r.With(mw.RepoPermissionMiddleware("repo:owner")).Route("/description", func(r chi.Router) { 154 - r.Put("/", s.RepoDescription) 155 - r.Get("/", s.RepoDescription) 156 - r.Get("/edit", s.RepoDescriptionEdit) 157 - }) 158 - r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) { 159 - r.Get("/", s.RepoSettings) 160 - r.With(mw.RepoPermissionMiddleware("repo:invite")).Put("/collaborator", s.AddCollaborator) 161 - r.With(mw.RepoPermissionMiddleware("repo:delete")).Delete("/delete", s.DeleteRepo) 162 - r.Put("/branches/default", s.SetDefaultBranch) 163 - }) 164 - }) 165 }) 166 }) 167 ··· 266 pulls := pulls.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config) 267 return pulls.Router(mw) 268 }
··· 9 "tangled.sh/tangled.sh/core/appview/middleware" 10 oauthhandler "tangled.sh/tangled.sh/core/appview/oauth/handler" 11 "tangled.sh/tangled.sh/core/appview/pulls" 12 + "tangled.sh/tangled.sh/core/appview/repo" 13 "tangled.sh/tangled.sh/core/appview/settings" 14 "tangled.sh/tangled.sh/core/appview/state/userutil" 15 ) ··· 70 r.With(mw.ResolveRepo()).Route("/{repo}", func(r chi.Router) { 71 r.Use(mw.GoImport()) 72 73 + r.Mount("/", s.RepoRouter(mw)) 74 75 r.Mount("/pulls", s.PullsRouter(mw)) 76 ··· 79 r.Post("/git-upload-pack", s.UploadPack) 80 r.Post("/git-receive-pack", s.ReceivePack) 81 82 }) 83 }) 84 ··· 183 pulls := pulls.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config) 184 return pulls.Router(mw) 185 } 186 + 187 + func (s *State) RepoRouter(mw *middleware.Middleware) http.Handler { 188 + repo := repo.New(s.oauth, s.repoResolver, s.pages, s.idResolver, s.db, s.config, s.posthog, s.enforcer) 189 + return repo.Router(mw) 190 + }