Monorepo for Tangled tangled.org

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

merged opened by oppi.li targeting master from push-qwwtnptuywsl
Labels
refactor
assignee

None yet.

Participants 3
AT URI
at://did:plc:qfpnj4og54vl56wngdriaxug/sh.tangled.repo.pull/3m4xpanyb4r22
+1516 -1456
Diff #2
+49
appview/repo/archive.go
··· 1 + package repo 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "net/url" 7 + "strings" 8 + 9 + "tangled.org/core/api/tangled" 10 + xrpcclient "tangled.org/core/appview/xrpcclient" 11 + 12 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 13 + "github.com/go-chi/chi/v5" 14 + "github.com/go-git/go-git/v5/plumbing" 15 + ) 16 + 17 + func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { 18 + l := rp.logger.With("handler", "DownloadArchive") 19 + ref := chi.URLParam(r, "ref") 20 + ref, _ = url.PathUnescape(ref) 21 + f, err := rp.repoResolver.Resolve(r) 22 + if err != nil { 23 + l.Error("failed to get repo and knot", "err", err) 24 + return 25 + } 26 + scheme := "http" 27 + if !rp.config.Core.Dev { 28 + scheme = "https" 29 + } 30 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 31 + xrpcc := &indigoxrpc.Client{ 32 + Host: host, 33 + } 34 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 35 + archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo) 36 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 37 + l.Error("failed to call XRPC repo.archive", "err", xrpcerr) 38 + rp.pages.Error503(w) 39 + return 40 + } 41 + // Set headers for file download, just pass along whatever the knot specifies 42 + safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 43 + filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename) 44 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 45 + w.Header().Set("Content-Type", "application/gzip") 46 + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) 47 + // Write the archive data directly 48 + w.Write(archiveBytes) 49 + }
+219
appview/repo/blob.go
··· 1 + package repo 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "net/http" 7 + "net/url" 8 + "path/filepath" 9 + "slices" 10 + "strings" 11 + 12 + "tangled.org/core/api/tangled" 13 + "tangled.org/core/appview/pages" 14 + "tangled.org/core/appview/pages/markup" 15 + xrpcclient "tangled.org/core/appview/xrpcclient" 16 + 17 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 18 + "github.com/go-chi/chi/v5" 19 + ) 20 + 21 + func (rp *Repo) Blob(w http.ResponseWriter, r *http.Request) { 22 + l := rp.logger.With("handler", "RepoBlob") 23 + f, err := rp.repoResolver.Resolve(r) 24 + if err != nil { 25 + l.Error("failed to get repo and knot", "err", err) 26 + return 27 + } 28 + ref := chi.URLParam(r, "ref") 29 + ref, _ = url.PathUnescape(ref) 30 + filePath := chi.URLParam(r, "*") 31 + filePath, _ = url.PathUnescape(filePath) 32 + scheme := "http" 33 + if !rp.config.Core.Dev { 34 + scheme = "https" 35 + } 36 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 37 + xrpcc := &indigoxrpc.Client{ 38 + Host: host, 39 + } 40 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 41 + resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo) 42 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 43 + l.Error("failed to call XRPC repo.blob", "err", xrpcerr) 44 + rp.pages.Error503(w) 45 + return 46 + } 47 + // Use XRPC response directly instead of converting to internal types 48 + var breadcrumbs [][]string 49 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))}) 50 + if filePath != "" { 51 + for idx, elem := range strings.Split(filePath, "/") { 52 + breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))}) 53 + } 54 + } 55 + showRendered := false 56 + renderToggle := false 57 + if markup.GetFormat(resp.Path) == markup.FormatMarkdown { 58 + renderToggle = true 59 + showRendered = r.URL.Query().Get("code") != "true" 60 + } 61 + var unsupported bool 62 + var isImage bool 63 + var isVideo bool 64 + var contentSrc string 65 + if resp.IsBinary != nil && *resp.IsBinary { 66 + ext := strings.ToLower(filepath.Ext(resp.Path)) 67 + switch ext { 68 + case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp": 69 + isImage = true 70 + case ".mp4", ".webm", ".ogg", ".mov", ".avi": 71 + isVideo = true 72 + default: 73 + unsupported = true 74 + } 75 + // fetch the raw binary content using sh.tangled.repo.blob xrpc 76 + repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 77 + baseURL := &url.URL{ 78 + Scheme: scheme, 79 + Host: f.Knot, 80 + Path: "/xrpc/sh.tangled.repo.blob", 81 + } 82 + query := baseURL.Query() 83 + query.Set("repo", repoName) 84 + query.Set("ref", ref) 85 + query.Set("path", filePath) 86 + query.Set("raw", "true") 87 + baseURL.RawQuery = query.Encode() 88 + blobURL := baseURL.String() 89 + contentSrc = blobURL 90 + if !rp.config.Core.Dev { 91 + contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL) 92 + } 93 + } 94 + lines := 0 95 + if resp.IsBinary == nil || !*resp.IsBinary { 96 + lines = strings.Count(resp.Content, "\n") + 1 97 + } 98 + var sizeHint uint64 99 + if resp.Size != nil { 100 + sizeHint = uint64(*resp.Size) 101 + } else { 102 + sizeHint = uint64(len(resp.Content)) 103 + } 104 + user := rp.oauth.GetUser(r) 105 + // Determine if content is binary (dereference pointer) 106 + isBinary := false 107 + if resp.IsBinary != nil { 108 + isBinary = *resp.IsBinary 109 + } 110 + rp.pages.RepoBlob(w, pages.RepoBlobParams{ 111 + LoggedInUser: user, 112 + RepoInfo: f.RepoInfo(user), 113 + BreadCrumbs: breadcrumbs, 114 + ShowRendered: showRendered, 115 + RenderToggle: renderToggle, 116 + Unsupported: unsupported, 117 + IsImage: isImage, 118 + IsVideo: isVideo, 119 + ContentSrc: contentSrc, 120 + RepoBlob_Output: resp, 121 + Contents: resp.Content, 122 + Lines: lines, 123 + SizeHint: sizeHint, 124 + IsBinary: isBinary, 125 + }) 126 + } 127 + 128 + func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 129 + l := rp.logger.With("handler", "RepoBlobRaw") 130 + f, err := rp.repoResolver.Resolve(r) 131 + if err != nil { 132 + l.Error("failed to get repo and knot", "err", err) 133 + w.WriteHeader(http.StatusBadRequest) 134 + return 135 + } 136 + ref := chi.URLParam(r, "ref") 137 + ref, _ = url.PathUnescape(ref) 138 + filePath := chi.URLParam(r, "*") 139 + filePath, _ = url.PathUnescape(filePath) 140 + scheme := "http" 141 + if !rp.config.Core.Dev { 142 + scheme = "https" 143 + } 144 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 145 + baseURL := &url.URL{ 146 + Scheme: scheme, 147 + Host: f.Knot, 148 + Path: "/xrpc/sh.tangled.repo.blob", 149 + } 150 + query := baseURL.Query() 151 + query.Set("repo", repo) 152 + query.Set("ref", ref) 153 + query.Set("path", filePath) 154 + query.Set("raw", "true") 155 + baseURL.RawQuery = query.Encode() 156 + blobURL := baseURL.String() 157 + req, err := http.NewRequest("GET", blobURL, nil) 158 + if err != nil { 159 + l.Error("failed to create request", "err", err) 160 + return 161 + } 162 + // forward the If-None-Match header 163 + if clientETag := r.Header.Get("If-None-Match"); clientETag != "" { 164 + req.Header.Set("If-None-Match", clientETag) 165 + } 166 + client := &http.Client{} 167 + resp, err := client.Do(req) 168 + if err != nil { 169 + l.Error("failed to reach knotserver", "err", err) 170 + rp.pages.Error503(w) 171 + return 172 + } 173 + defer resp.Body.Close() 174 + // forward 304 not modified 175 + if resp.StatusCode == http.StatusNotModified { 176 + w.WriteHeader(http.StatusNotModified) 177 + return 178 + } 179 + if resp.StatusCode != http.StatusOK { 180 + l.Error("knotserver returned non-OK status for raw blob", "url", blobURL, "statuscode", resp.StatusCode) 181 + w.WriteHeader(resp.StatusCode) 182 + _, _ = io.Copy(w, resp.Body) 183 + return 184 + } 185 + contentType := resp.Header.Get("Content-Type") 186 + body, err := io.ReadAll(resp.Body) 187 + if err != nil { 188 + l.Error("error reading response body from knotserver", "err", err) 189 + w.WriteHeader(http.StatusInternalServerError) 190 + return 191 + } 192 + if strings.HasPrefix(contentType, "text/") || isTextualMimeType(contentType) { 193 + // serve all textual content as text/plain 194 + w.Header().Set("Content-Type", "text/plain; charset=utf-8") 195 + w.Write(body) 196 + } else if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") { 197 + // serve images and videos with their original content type 198 + w.Header().Set("Content-Type", contentType) 199 + w.Write(body) 200 + } else { 201 + w.WriteHeader(http.StatusUnsupportedMediaType) 202 + w.Write([]byte("unsupported content type")) 203 + return 204 + } 205 + } 206 + 207 + func isTextualMimeType(mimeType string) bool { 208 + textualTypes := []string{ 209 + "application/json", 210 + "application/xml", 211 + "application/yaml", 212 + "application/x-yaml", 213 + "application/toml", 214 + "application/javascript", 215 + "application/ecmascript", 216 + "message/", 217 + } 218 + return slices.Contains(textualTypes, mimeType) 219 + }
+95
appview/repo/branches.go
··· 1 + package repo 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + 8 + "tangled.org/core/api/tangled" 9 + "tangled.org/core/appview/oauth" 10 + "tangled.org/core/appview/pages" 11 + xrpcclient "tangled.org/core/appview/xrpcclient" 12 + "tangled.org/core/types" 13 + 14 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 15 + ) 16 + 17 + func (rp *Repo) Branches(w http.ResponseWriter, r *http.Request) { 18 + l := rp.logger.With("handler", "RepoBranches") 19 + f, err := rp.repoResolver.Resolve(r) 20 + if err != nil { 21 + l.Error("failed to get repo and knot", "err", err) 22 + return 23 + } 24 + scheme := "http" 25 + if !rp.config.Core.Dev { 26 + scheme = "https" 27 + } 28 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 29 + xrpcc := &indigoxrpc.Client{ 30 + Host: host, 31 + } 32 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 33 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 34 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 35 + l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 36 + rp.pages.Error503(w) 37 + return 38 + } 39 + var result types.RepoBranchesResponse 40 + if err := json.Unmarshal(xrpcBytes, &result); err != nil { 41 + l.Error("failed to decode XRPC response", "err", err) 42 + rp.pages.Error503(w) 43 + return 44 + } 45 + sortBranches(result.Branches) 46 + user := rp.oauth.GetUser(r) 47 + rp.pages.RepoBranches(w, pages.RepoBranchesParams{ 48 + LoggedInUser: user, 49 + RepoInfo: f.RepoInfo(user), 50 + RepoBranchesResponse: result, 51 + }) 52 + } 53 + 54 + func (rp *Repo) DeleteBranch(w http.ResponseWriter, r *http.Request) { 55 + l := rp.logger.With("handler", "DeleteBranch") 56 + f, err := rp.repoResolver.Resolve(r) 57 + if err != nil { 58 + l.Error("failed to get repo and knot", "err", err) 59 + return 60 + } 61 + noticeId := "delete-branch-error" 62 + fail := func(msg string, err error) { 63 + l.Error(msg, "err", err) 64 + rp.pages.Notice(w, noticeId, msg) 65 + } 66 + branch := r.FormValue("branch") 67 + if branch == "" { 68 + fail("No branch provided.", nil) 69 + return 70 + } 71 + client, err := rp.oauth.ServiceClient( 72 + r, 73 + oauth.WithService(f.Knot), 74 + oauth.WithLxm(tangled.RepoDeleteBranchNSID), 75 + oauth.WithDev(rp.config.Core.Dev), 76 + ) 77 + if err != nil { 78 + fail("Failed to connect to knotserver", nil) 79 + return 80 + } 81 + err = tangled.RepoDeleteBranch( 82 + r.Context(), 83 + client, 84 + &tangled.RepoDeleteBranch_Input{ 85 + Branch: branch, 86 + Repo: f.RepoAt().String(), 87 + }, 88 + ) 89 + if err := xrpcclient.HandleXrpcErr(err); err != nil { 90 + fail(fmt.Sprintf("Failed to delete branch: %s", err), err) 91 + return 92 + } 93 + l.Error("deleted branch from knot", "branch", branch, "repo", f.RepoAt()) 94 + rp.pages.HxRefresh(w) 95 + }
+214
appview/repo/compare.go
··· 1 + package repo 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + "net/url" 8 + "strings" 9 + 10 + "tangled.org/core/api/tangled" 11 + "tangled.org/core/appview/pages" 12 + xrpcclient "tangled.org/core/appview/xrpcclient" 13 + "tangled.org/core/patchutil" 14 + "tangled.org/core/types" 15 + 16 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 17 + "github.com/go-chi/chi/v5" 18 + ) 19 + 20 + func (rp *Repo) CompareNew(w http.ResponseWriter, r *http.Request) { 21 + l := rp.logger.With("handler", "RepoCompareNew") 22 + 23 + user := rp.oauth.GetUser(r) 24 + f, err := rp.repoResolver.Resolve(r) 25 + if err != nil { 26 + l.Error("failed to get repo and knot", "err", err) 27 + return 28 + } 29 + 30 + scheme := "http" 31 + if !rp.config.Core.Dev { 32 + scheme = "https" 33 + } 34 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 35 + xrpcc := &indigoxrpc.Client{ 36 + Host: host, 37 + } 38 + 39 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 40 + branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 41 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 42 + l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 43 + rp.pages.Error503(w) 44 + return 45 + } 46 + 47 + var branchResult types.RepoBranchesResponse 48 + if err := json.Unmarshal(branchBytes, &branchResult); err != nil { 49 + l.Error("failed to decode XRPC branches response", "err", err) 50 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 51 + return 52 + } 53 + branches := branchResult.Branches 54 + 55 + sortBranches(branches) 56 + 57 + var defaultBranch string 58 + for _, b := range branches { 59 + if b.IsDefault { 60 + defaultBranch = b.Name 61 + } 62 + } 63 + 64 + base := defaultBranch 65 + head := defaultBranch 66 + 67 + params := r.URL.Query() 68 + queryBase := params.Get("base") 69 + queryHead := params.Get("head") 70 + if queryBase != "" { 71 + base = queryBase 72 + } 73 + if queryHead != "" { 74 + head = queryHead 75 + } 76 + 77 + tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 78 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 79 + l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 80 + rp.pages.Error503(w) 81 + return 82 + } 83 + 84 + var tags types.RepoTagsResponse 85 + if err := json.Unmarshal(tagBytes, &tags); err != nil { 86 + l.Error("failed to decode XRPC tags response", "err", err) 87 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 88 + return 89 + } 90 + 91 + repoinfo := f.RepoInfo(user) 92 + 93 + rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{ 94 + LoggedInUser: user, 95 + RepoInfo: repoinfo, 96 + Branches: branches, 97 + Tags: tags.Tags, 98 + Base: base, 99 + Head: head, 100 + }) 101 + } 102 + 103 + func (rp *Repo) Compare(w http.ResponseWriter, r *http.Request) { 104 + l := rp.logger.With("handler", "RepoCompare") 105 + 106 + user := rp.oauth.GetUser(r) 107 + f, err := rp.repoResolver.Resolve(r) 108 + if err != nil { 109 + l.Error("failed to get repo and knot", "err", err) 110 + return 111 + } 112 + 113 + var diffOpts types.DiffOpts 114 + if d := r.URL.Query().Get("diff"); d == "split" { 115 + diffOpts.Split = true 116 + } 117 + 118 + // if user is navigating to one of 119 + // /compare/{base}/{head} 120 + // /compare/{base}...{head} 121 + base := chi.URLParam(r, "base") 122 + head := chi.URLParam(r, "head") 123 + if base == "" && head == "" { 124 + rest := chi.URLParam(r, "*") // master...feature/xyz 125 + parts := strings.SplitN(rest, "...", 2) 126 + if len(parts) == 2 { 127 + base = parts[0] 128 + head = parts[1] 129 + } 130 + } 131 + 132 + base, _ = url.PathUnescape(base) 133 + head, _ = url.PathUnescape(head) 134 + 135 + if base == "" || head == "" { 136 + l.Error("invalid comparison") 137 + rp.pages.Error404(w) 138 + return 139 + } 140 + 141 + scheme := "http" 142 + if !rp.config.Core.Dev { 143 + scheme = "https" 144 + } 145 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 146 + xrpcc := &indigoxrpc.Client{ 147 + Host: host, 148 + } 149 + 150 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 151 + 152 + branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 153 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 154 + l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 155 + rp.pages.Error503(w) 156 + return 157 + } 158 + 159 + var branches types.RepoBranchesResponse 160 + if err := json.Unmarshal(branchBytes, &branches); err != nil { 161 + l.Error("failed to decode XRPC branches response", "err", err) 162 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 163 + return 164 + } 165 + 166 + tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 167 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 168 + l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 169 + rp.pages.Error503(w) 170 + return 171 + } 172 + 173 + var tags types.RepoTagsResponse 174 + if err := json.Unmarshal(tagBytes, &tags); err != nil { 175 + l.Error("failed to decode XRPC tags response", "err", err) 176 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 177 + return 178 + } 179 + 180 + compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head) 181 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 182 + l.Error("failed to call XRPC repo.compare", "err", xrpcerr) 183 + rp.pages.Error503(w) 184 + return 185 + } 186 + 187 + var formatPatch types.RepoFormatPatchResponse 188 + if err := json.Unmarshal(compareBytes, &formatPatch); err != nil { 189 + l.Error("failed to decode XRPC compare response", "err", err) 190 + rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 191 + return 192 + } 193 + 194 + var diff types.NiceDiff 195 + if formatPatch.CombinedPatchRaw != "" { 196 + diff = patchutil.AsNiceDiff(formatPatch.CombinedPatchRaw, base) 197 + } else { 198 + diff = patchutil.AsNiceDiff(formatPatch.FormatPatchRaw, base) 199 + } 200 + 201 + repoinfo := f.RepoInfo(user) 202 + 203 + rp.pages.RepoCompare(w, pages.RepoCompareParams{ 204 + LoggedInUser: user, 205 + RepoInfo: repoinfo, 206 + Branches: branches.Branches, 207 + Tags: tags.Tags, 208 + Base: base, 209 + Head: head, 210 + Diff: &diff, 211 + DiffOpts: diffOpts, 212 + }) 213 + 214 + }
+1 -1
appview/repo/feed.go
··· 146 146 return fmt.Sprintf("%s in %s", base, repoName) 147 147 } 148 148 149 - func (rp *Repo) RepoAtomFeed(w http.ResponseWriter, r *http.Request) { 149 + func (rp *Repo) AtomFeed(w http.ResponseWriter, r *http.Request) { 150 150 f, err := rp.repoResolver.Resolve(r) 151 151 if err != nil { 152 152 log.Println("failed to fully resolve repo:", err)
+1 -1
appview/repo/index.go
··· 30 30 "github.com/go-enry/go-enry/v2" 31 31 ) 32 32 33 - func (rp *Repo) RepoIndex(w http.ResponseWriter, r *http.Request) { 33 + func (rp *Repo) Index(w http.ResponseWriter, r *http.Request) { 34 34 l := rp.logger.With("handler", "RepoIndex") 35 35 36 36 ref := chi.URLParam(r, "ref")
+223
appview/repo/log.go
··· 1 + package repo 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + "net/url" 8 + "strconv" 9 + 10 + "tangled.org/core/api/tangled" 11 + "tangled.org/core/appview/commitverify" 12 + "tangled.org/core/appview/db" 13 + "tangled.org/core/appview/models" 14 + "tangled.org/core/appview/pages" 15 + xrpcclient "tangled.org/core/appview/xrpcclient" 16 + "tangled.org/core/types" 17 + 18 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 19 + "github.com/go-chi/chi/v5" 20 + "github.com/go-git/go-git/v5/plumbing" 21 + ) 22 + 23 + func (rp *Repo) Log(w http.ResponseWriter, r *http.Request) { 24 + l := rp.logger.With("handler", "RepoLog") 25 + 26 + f, err := rp.repoResolver.Resolve(r) 27 + if err != nil { 28 + l.Error("failed to fully resolve repo", "err", err) 29 + return 30 + } 31 + 32 + page := 1 33 + if r.URL.Query().Get("page") != "" { 34 + page, err = strconv.Atoi(r.URL.Query().Get("page")) 35 + if err != nil { 36 + page = 1 37 + } 38 + } 39 + 40 + ref := chi.URLParam(r, "ref") 41 + ref, _ = url.PathUnescape(ref) 42 + 43 + scheme := "http" 44 + if !rp.config.Core.Dev { 45 + scheme = "https" 46 + } 47 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 48 + xrpcc := &indigoxrpc.Client{ 49 + Host: host, 50 + } 51 + 52 + limit := int64(60) 53 + cursor := "" 54 + if page > 1 { 55 + // Convert page number to cursor (offset) 56 + offset := (page - 1) * int(limit) 57 + cursor = strconv.Itoa(offset) 58 + } 59 + 60 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 61 + xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo) 62 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 63 + l.Error("failed to call XRPC repo.log", "err", xrpcerr) 64 + rp.pages.Error503(w) 65 + return 66 + } 67 + 68 + var xrpcResp types.RepoLogResponse 69 + if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil { 70 + l.Error("failed to decode XRPC response", "err", err) 71 + rp.pages.Error503(w) 72 + return 73 + } 74 + 75 + tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 76 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 77 + l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 78 + rp.pages.Error503(w) 79 + return 80 + } 81 + 82 + tagMap := make(map[string][]string) 83 + if tagBytes != nil { 84 + var tagResp types.RepoTagsResponse 85 + if err := json.Unmarshal(tagBytes, &tagResp); err == nil { 86 + for _, tag := range tagResp.Tags { 87 + hash := tag.Hash 88 + if tag.Tag != nil { 89 + hash = tag.Tag.Target.String() 90 + } 91 + tagMap[hash] = append(tagMap[hash], tag.Name) 92 + } 93 + } 94 + } 95 + 96 + branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 97 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 98 + l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 99 + rp.pages.Error503(w) 100 + return 101 + } 102 + 103 + if branchBytes != nil { 104 + var branchResp types.RepoBranchesResponse 105 + if err := json.Unmarshal(branchBytes, &branchResp); err == nil { 106 + for _, branch := range branchResp.Branches { 107 + tagMap[branch.Hash] = append(tagMap[branch.Hash], branch.Name) 108 + } 109 + } 110 + } 111 + 112 + user := rp.oauth.GetUser(r) 113 + 114 + emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true) 115 + if err != nil { 116 + l.Error("failed to fetch email to did mapping", "err", err) 117 + } 118 + 119 + vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits) 120 + if err != nil { 121 + l.Error("failed to GetVerifiedObjectCommits", "err", err) 122 + } 123 + 124 + repoInfo := f.RepoInfo(user) 125 + 126 + var shas []string 127 + for _, c := range xrpcResp.Commits { 128 + shas = append(shas, c.Hash.String()) 129 + } 130 + pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas) 131 + if err != nil { 132 + l.Error("failed to getPipelineStatuses", "err", err) 133 + // non-fatal 134 + } 135 + 136 + rp.pages.RepoLog(w, pages.RepoLogParams{ 137 + LoggedInUser: user, 138 + TagMap: tagMap, 139 + RepoInfo: repoInfo, 140 + RepoLogResponse: xrpcResp, 141 + EmailToDid: emailToDidMap, 142 + VerifiedCommits: vc, 143 + Pipelines: pipelines, 144 + }) 145 + } 146 + 147 + func (rp *Repo) Commit(w http.ResponseWriter, r *http.Request) { 148 + l := rp.logger.With("handler", "RepoCommit") 149 + 150 + f, err := rp.repoResolver.Resolve(r) 151 + if err != nil { 152 + l.Error("failed to fully resolve repo", "err", err) 153 + return 154 + } 155 + ref := chi.URLParam(r, "ref") 156 + ref, _ = url.PathUnescape(ref) 157 + 158 + var diffOpts types.DiffOpts 159 + if d := r.URL.Query().Get("diff"); d == "split" { 160 + diffOpts.Split = true 161 + } 162 + 163 + if !plumbing.IsHash(ref) { 164 + rp.pages.Error404(w) 165 + return 166 + } 167 + 168 + scheme := "http" 169 + if !rp.config.Core.Dev { 170 + scheme = "https" 171 + } 172 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 173 + xrpcc := &indigoxrpc.Client{ 174 + Host: host, 175 + } 176 + 177 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 178 + xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo) 179 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 180 + l.Error("failed to call XRPC repo.diff", "err", xrpcerr) 181 + rp.pages.Error503(w) 182 + return 183 + } 184 + 185 + var result types.RepoCommitResponse 186 + if err := json.Unmarshal(xrpcBytes, &result); err != nil { 187 + l.Error("failed to decode XRPC response", "err", err) 188 + rp.pages.Error503(w) 189 + return 190 + } 191 + 192 + emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Committer.Email, result.Diff.Commit.Author.Email}, true) 193 + if err != nil { 194 + l.Error("failed to get email to did mapping", "err", err) 195 + } 196 + 197 + vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff}) 198 + if err != nil { 199 + l.Error("failed to GetVerifiedCommits", "err", err) 200 + } 201 + 202 + user := rp.oauth.GetUser(r) 203 + repoInfo := f.RepoInfo(user) 204 + pipelines, err := getPipelineStatuses(rp.db, repoInfo, []string{result.Diff.Commit.This}) 205 + if err != nil { 206 + l.Error("failed to getPipelineStatuses", "err", err) 207 + // non-fatal 208 + } 209 + var pipeline *models.Pipeline 210 + if p, ok := pipelines[result.Diff.Commit.This]; ok { 211 + pipeline = &p 212 + } 213 + 214 + rp.pages.RepoCommit(w, pages.RepoCommitParams{ 215 + LoggedInUser: user, 216 + RepoInfo: f.RepoInfo(user), 217 + RepoCommitResponse: result, 218 + EmailToDid: emailToDidMap, 219 + VerifiedCommit: vc, 220 + Pipeline: pipeline, 221 + DiffOpts: diffOpts, 222 + }) 223 + }
+1 -1
appview/repo/opengraph.go
··· 327 327 return nil 328 328 } 329 329 330 - func (rp *Repo) RepoOpenGraphSummary(w http.ResponseWriter, r *http.Request) { 330 + func (rp *Repo) Opengraph(w http.ResponseWriter, r *http.Request) { 331 331 f, err := rp.repoResolver.Resolve(r) 332 332 if err != nil { 333 333 log.Println("failed to get repo and knot", err)
+71 -1439
appview/repo/repo.go
··· 3 3 import ( 4 4 "context" 5 5 "database/sql" 6 - "encoding/json" 7 6 "errors" 8 7 "fmt" 9 - "io" 10 8 "log/slog" 11 9 "net/http" 12 10 "net/url" 13 - "path/filepath" 14 11 "slices" 15 - "strconv" 16 12 "strings" 17 13 "time" 18 14 19 15 "tangled.org/core/api/tangled" 20 - "tangled.org/core/appview/commitverify" 21 16 "tangled.org/core/appview/config" 22 17 "tangled.org/core/appview/db" 23 18 "tangled.org/core/appview/models" 24 19 "tangled.org/core/appview/notify" 25 20 "tangled.org/core/appview/oauth" 26 21 "tangled.org/core/appview/pages" 27 - "tangled.org/core/appview/pages/markup" 28 22 "tangled.org/core/appview/reporesolver" 29 23 "tangled.org/core/appview/validator" 30 24 xrpcclient "tangled.org/core/appview/xrpcclient" 31 25 "tangled.org/core/eventconsumer" 32 26 "tangled.org/core/idresolver" 33 - "tangled.org/core/patchutil" 34 27 "tangled.org/core/rbac" 35 28 "tangled.org/core/tid" 36 - "tangled.org/core/types" 37 29 "tangled.org/core/xrpc/serviceauth" 38 30 39 31 comatproto "github.com/bluesky-social/indigo/api/atproto" 40 32 atpclient "github.com/bluesky-social/indigo/atproto/client" 41 33 "github.com/bluesky-social/indigo/atproto/syntax" 42 34 lexutil "github.com/bluesky-social/indigo/lex/util" 43 - indigoxrpc "github.com/bluesky-social/indigo/xrpc" 44 35 securejoin "github.com/cyphar/filepath-securejoin" 45 36 "github.com/go-chi/chi/v5" 46 - "github.com/go-git/go-git/v5/plumbing" 47 37 ) 48 38 49 39 type Repo struct { ··· 88 78 } 89 79 } 90 80 91 - func (rp *Repo) DownloadArchive(w http.ResponseWriter, r *http.Request) { 92 - l := rp.logger.With("handler", "DownloadArchive") 93 - 94 - ref := chi.URLParam(r, "ref") 95 - ref, _ = url.PathUnescape(ref) 96 - 97 - f, err := rp.repoResolver.Resolve(r) 98 - if err != nil { 99 - l.Error("failed to get repo and knot", "err", err) 100 - return 101 - } 102 - 103 - scheme := "http" 104 - if !rp.config.Core.Dev { 105 - scheme = "https" 106 - } 107 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 108 - xrpcc := &indigoxrpc.Client{ 109 - Host: host, 110 - } 111 - 112 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 113 - archiveBytes, err := tangled.RepoArchive(r.Context(), xrpcc, "tar.gz", "", ref, repo) 114 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 115 - l.Error("failed to call XRPC repo.archive", "err", xrpcerr) 116 - rp.pages.Error503(w) 117 - return 118 - } 119 - 120 - // Set headers for file download, just pass along whatever the knot specifies 121 - safeRefFilename := strings.ReplaceAll(plumbing.ReferenceName(ref).Short(), "/", "-") 122 - filename := fmt.Sprintf("%s-%s.tar.gz", f.Name, safeRefFilename) 123 - w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) 124 - w.Header().Set("Content-Type", "application/gzip") 125 - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(archiveBytes))) 126 - 127 - // Write the archive data directly 128 - w.Write(archiveBytes) 129 - } 130 - 131 - func (rp *Repo) RepoLog(w http.ResponseWriter, r *http.Request) { 132 - l := rp.logger.With("handler", "RepoLog") 133 - 134 - f, err := rp.repoResolver.Resolve(r) 135 - if err != nil { 136 - l.Error("failed to fully resolve repo", "err", err) 137 - return 138 - } 139 - 140 - page := 1 141 - if r.URL.Query().Get("page") != "" { 142 - page, err = strconv.Atoi(r.URL.Query().Get("page")) 143 - if err != nil { 144 - page = 1 145 - } 146 - } 147 - 148 - ref := chi.URLParam(r, "ref") 149 - ref, _ = url.PathUnescape(ref) 150 - 151 - scheme := "http" 152 - if !rp.config.Core.Dev { 153 - scheme = "https" 154 - } 155 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 156 - xrpcc := &indigoxrpc.Client{ 157 - Host: host, 158 - } 159 - 160 - limit := int64(60) 161 - cursor := "" 162 - if page > 1 { 163 - // Convert page number to cursor (offset) 164 - offset := (page - 1) * int(limit) 165 - cursor = strconv.Itoa(offset) 166 - } 167 - 168 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 169 - xrpcBytes, err := tangled.RepoLog(r.Context(), xrpcc, cursor, limit, "", ref, repo) 170 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 171 - l.Error("failed to call XRPC repo.log", "err", xrpcerr) 172 - rp.pages.Error503(w) 173 - return 174 - } 175 - 176 - var xrpcResp types.RepoLogResponse 177 - if err := json.Unmarshal(xrpcBytes, &xrpcResp); err != nil { 178 - l.Error("failed to decode XRPC response", "err", err) 179 - rp.pages.Error503(w) 180 - return 181 - } 182 - 183 - tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 184 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 185 - l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 186 - rp.pages.Error503(w) 187 - return 188 - } 189 - 190 - tagMap := make(map[string][]string) 191 - if tagBytes != nil { 192 - var tagResp types.RepoTagsResponse 193 - if err := json.Unmarshal(tagBytes, &tagResp); err == nil { 194 - for _, tag := range tagResp.Tags { 195 - hash := tag.Hash 196 - if tag.Tag != nil { 197 - hash = tag.Tag.Target.String() 198 - } 199 - tagMap[hash] = append(tagMap[hash], tag.Name) 200 - } 201 - } 202 - } 203 - 204 - branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 205 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 206 - l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 207 - rp.pages.Error503(w) 208 - return 209 - } 210 - 211 - if branchBytes != nil { 212 - var branchResp types.RepoBranchesResponse 213 - if err := json.Unmarshal(branchBytes, &branchResp); err == nil { 214 - for _, branch := range branchResp.Branches { 215 - tagMap[branch.Hash] = append(tagMap[branch.Hash], branch.Name) 216 - } 217 - } 218 - } 219 - 220 - user := rp.oauth.GetUser(r) 221 - 222 - emailToDidMap, err := db.GetEmailToDid(rp.db, uniqueEmails(xrpcResp.Commits), true) 223 - if err != nil { 224 - l.Error("failed to fetch email to did mapping", "err", err) 225 - } 226 - 227 - vc, err := commitverify.GetVerifiedObjectCommits(rp.db, emailToDidMap, xrpcResp.Commits) 228 - if err != nil { 229 - l.Error("failed to GetVerifiedObjectCommits", "err", err) 230 - } 231 - 232 - repoInfo := f.RepoInfo(user) 233 - 234 - var shas []string 235 - for _, c := range xrpcResp.Commits { 236 - shas = append(shas, c.Hash.String()) 237 - } 238 - pipelines, err := getPipelineStatuses(rp.db, repoInfo, shas) 239 - if err != nil { 240 - l.Error("failed to getPipelineStatuses", "err", err) 241 - // non-fatal 242 - } 243 - 244 - rp.pages.RepoLog(w, pages.RepoLogParams{ 245 - LoggedInUser: user, 246 - TagMap: tagMap, 247 - RepoInfo: repoInfo, 248 - RepoLogResponse: xrpcResp, 249 - EmailToDid: emailToDidMap, 250 - VerifiedCommits: vc, 251 - Pipelines: pipelines, 252 - }) 253 - } 254 - 255 - func (rp *Repo) RepoCommit(w http.ResponseWriter, r *http.Request) { 256 - l := rp.logger.With("handler", "RepoCommit") 257 - 258 - f, err := rp.repoResolver.Resolve(r) 259 - if err != nil { 260 - l.Error("failed to fully resolve repo", "err", err) 261 - return 262 - } 263 - ref := chi.URLParam(r, "ref") 264 - ref, _ = url.PathUnescape(ref) 265 - 266 - var diffOpts types.DiffOpts 267 - if d := r.URL.Query().Get("diff"); d == "split" { 268 - diffOpts.Split = true 269 - } 270 - 271 - if !plumbing.IsHash(ref) { 272 - rp.pages.Error404(w) 273 - return 274 - } 275 - 276 - scheme := "http" 277 - if !rp.config.Core.Dev { 278 - scheme = "https" 279 - } 280 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 281 - xrpcc := &indigoxrpc.Client{ 282 - Host: host, 283 - } 284 - 285 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 286 - xrpcBytes, err := tangled.RepoDiff(r.Context(), xrpcc, ref, repo) 287 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 288 - l.Error("failed to call XRPC repo.diff", "err", xrpcerr) 289 - rp.pages.Error503(w) 290 - return 291 - } 292 - 293 - var result types.RepoCommitResponse 294 - if err := json.Unmarshal(xrpcBytes, &result); err != nil { 295 - l.Error("failed to decode XRPC response", "err", err) 296 - rp.pages.Error503(w) 297 - return 298 - } 299 - 300 - emailToDidMap, err := db.GetEmailToDid(rp.db, []string{result.Diff.Commit.Committer.Email, result.Diff.Commit.Author.Email}, true) 301 - if err != nil { 302 - l.Error("failed to get email to did mapping", "err", err) 303 - } 304 - 305 - vc, err := commitverify.GetVerifiedCommits(rp.db, emailToDidMap, []types.NiceDiff{*result.Diff}) 306 - if err != nil { 307 - l.Error("failed to GetVerifiedCommits", "err", err) 308 - } 309 - 310 - user := rp.oauth.GetUser(r) 311 - repoInfo := f.RepoInfo(user) 312 - pipelines, err := getPipelineStatuses(rp.db, repoInfo, []string{result.Diff.Commit.This}) 313 - if err != nil { 314 - l.Error("failed to getPipelineStatuses", "err", err) 315 - // non-fatal 316 - } 317 - var pipeline *models.Pipeline 318 - if p, ok := pipelines[result.Diff.Commit.This]; ok { 319 - pipeline = &p 320 - } 321 - 322 - rp.pages.RepoCommit(w, pages.RepoCommitParams{ 323 - LoggedInUser: user, 324 - RepoInfo: f.RepoInfo(user), 325 - RepoCommitResponse: result, 326 - EmailToDid: emailToDidMap, 327 - VerifiedCommit: vc, 328 - Pipeline: pipeline, 329 - DiffOpts: diffOpts, 330 - }) 331 - } 332 - 333 - func (rp *Repo) RepoTree(w http.ResponseWriter, r *http.Request) { 334 - l := rp.logger.With("handler", "RepoTree") 335 - 336 - f, err := rp.repoResolver.Resolve(r) 337 - if err != nil { 338 - l.Error("failed to fully resolve repo", "err", err) 339 - return 340 - } 341 - 342 - ref := chi.URLParam(r, "ref") 343 - ref, _ = url.PathUnescape(ref) 344 - 345 - // if the tree path has a trailing slash, let's strip it 346 - // so we don't 404 347 - treePath := chi.URLParam(r, "*") 348 - treePath, _ = url.PathUnescape(treePath) 349 - treePath = strings.TrimSuffix(treePath, "/") 350 - 351 - scheme := "http" 352 - if !rp.config.Core.Dev { 353 - scheme = "https" 354 - } 355 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 356 - xrpcc := &indigoxrpc.Client{ 357 - Host: host, 358 - } 359 - 360 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 361 - xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo) 362 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 363 - l.Error("failed to call XRPC repo.tree", "err", xrpcerr) 364 - rp.pages.Error503(w) 365 - return 366 - } 367 - 368 - // Convert XRPC response to internal types.RepoTreeResponse 369 - files := make([]types.NiceTree, len(xrpcResp.Files)) 370 - for i, xrpcFile := range xrpcResp.Files { 371 - file := types.NiceTree{ 372 - Name: xrpcFile.Name, 373 - Mode: xrpcFile.Mode, 374 - Size: int64(xrpcFile.Size), 375 - IsFile: xrpcFile.Is_file, 376 - IsSubtree: xrpcFile.Is_subtree, 377 - } 378 - 379 - // Convert last commit info if present 380 - if xrpcFile.Last_commit != nil { 381 - commitWhen, _ := time.Parse(time.RFC3339, xrpcFile.Last_commit.When) 382 - file.LastCommit = &types.LastCommitInfo{ 383 - Hash: plumbing.NewHash(xrpcFile.Last_commit.Hash), 384 - Message: xrpcFile.Last_commit.Message, 385 - When: commitWhen, 386 - } 387 - } 388 - 389 - files[i] = file 390 - } 391 - 392 - result := types.RepoTreeResponse{ 393 - Ref: xrpcResp.Ref, 394 - Files: files, 395 - } 396 - 397 - if xrpcResp.Parent != nil { 398 - result.Parent = *xrpcResp.Parent 399 - } 400 - if xrpcResp.Dotdot != nil { 401 - result.DotDot = *xrpcResp.Dotdot 402 - } 403 - if xrpcResp.Readme != nil { 404 - result.ReadmeFileName = xrpcResp.Readme.Filename 405 - result.Readme = xrpcResp.Readme.Contents 406 - } 407 - 408 - // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated, 409 - // so we can safely redirect to the "parent" (which is the same file). 410 - if len(result.Files) == 0 && result.Parent == treePath { 411 - redirectTo := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), url.PathEscape(ref), result.Parent) 412 - http.Redirect(w, r, redirectTo, http.StatusFound) 413 - return 414 - } 415 - 416 - user := rp.oauth.GetUser(r) 417 - 418 - var breadcrumbs [][]string 419 - breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))}) 420 - if treePath != "" { 421 - for idx, elem := range strings.Split(treePath, "/") { 422 - breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))}) 423 - } 424 - } 425 - 426 - sortFiles(result.Files) 427 - 428 - rp.pages.RepoTree(w, pages.RepoTreeParams{ 429 - LoggedInUser: user, 430 - BreadCrumbs: breadcrumbs, 431 - TreePath: treePath, 432 - RepoInfo: f.RepoInfo(user), 433 - RepoTreeResponse: result, 434 - }) 435 - } 436 - 437 - func (rp *Repo) RepoTags(w http.ResponseWriter, r *http.Request) { 438 - l := rp.logger.With("handler", "RepoTags") 439 - 440 - f, err := rp.repoResolver.Resolve(r) 441 - if err != nil { 442 - l.Error("failed to get repo and knot", "err", err) 443 - return 444 - } 445 - 446 - scheme := "http" 447 - if !rp.config.Core.Dev { 448 - scheme = "https" 449 - } 450 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 451 - xrpcc := &indigoxrpc.Client{ 452 - Host: host, 453 - } 454 - 455 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 456 - xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 457 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 458 - l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 459 - rp.pages.Error503(w) 460 - return 461 - } 462 - 463 - var result types.RepoTagsResponse 464 - if err := json.Unmarshal(xrpcBytes, &result); err != nil { 465 - l.Error("failed to decode XRPC response", "err", err) 466 - rp.pages.Error503(w) 467 - return 468 - } 469 - 470 - artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt())) 471 - if err != nil { 472 - l.Error("failed grab artifacts", "err", err) 473 - return 474 - } 475 - 476 - // convert artifacts to map for easy UI building 477 - artifactMap := make(map[plumbing.Hash][]models.Artifact) 478 - for _, a := range artifacts { 479 - artifactMap[a.Tag] = append(artifactMap[a.Tag], a) 480 - } 481 - 482 - var danglingArtifacts []models.Artifact 483 - for _, a := range artifacts { 484 - found := false 485 - for _, t := range result.Tags { 486 - if t.Tag != nil { 487 - if t.Tag.Hash == a.Tag { 488 - found = true 489 - } 490 - } 491 - } 492 - 493 - if !found { 494 - danglingArtifacts = append(danglingArtifacts, a) 495 - } 496 - } 497 - 498 - user := rp.oauth.GetUser(r) 499 - rp.pages.RepoTags(w, pages.RepoTagsParams{ 500 - LoggedInUser: user, 501 - RepoInfo: f.RepoInfo(user), 502 - RepoTagsResponse: result, 503 - ArtifactMap: artifactMap, 504 - DanglingArtifacts: danglingArtifacts, 505 - }) 506 - } 507 - 508 - func (rp *Repo) RepoBranches(w http.ResponseWriter, r *http.Request) { 509 - l := rp.logger.With("handler", "RepoBranches") 510 - 511 - f, err := rp.repoResolver.Resolve(r) 512 - if err != nil { 513 - l.Error("failed to get repo and knot", "err", err) 514 - return 515 - } 516 - 517 - scheme := "http" 518 - if !rp.config.Core.Dev { 519 - scheme = "https" 520 - } 521 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 522 - xrpcc := &indigoxrpc.Client{ 523 - Host: host, 524 - } 525 - 526 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 527 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 528 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 529 - l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 530 - rp.pages.Error503(w) 531 - return 532 - } 533 - 534 - var result types.RepoBranchesResponse 535 - if err := json.Unmarshal(xrpcBytes, &result); err != nil { 536 - l.Error("failed to decode XRPC response", "err", err) 537 - rp.pages.Error503(w) 538 - return 539 - } 540 - 541 - sortBranches(result.Branches) 542 - 543 - user := rp.oauth.GetUser(r) 544 - rp.pages.RepoBranches(w, pages.RepoBranchesParams{ 545 - LoggedInUser: user, 546 - RepoInfo: f.RepoInfo(user), 547 - RepoBranchesResponse: result, 548 - }) 549 - } 550 - 551 - func (rp *Repo) DeleteBranch(w http.ResponseWriter, r *http.Request) { 552 - l := rp.logger.With("handler", "DeleteBranch") 553 - 554 - f, err := rp.repoResolver.Resolve(r) 555 - if err != nil { 556 - l.Error("failed to get repo and knot", "err", err) 557 - return 558 - } 559 - 560 - noticeId := "delete-branch-error" 561 - fail := func(msg string, err error) { 562 - l.Error(msg, "err", err) 563 - rp.pages.Notice(w, noticeId, msg) 564 - } 565 - 566 - branch := r.FormValue("branch") 567 - if branch == "" { 568 - fail("No branch provided.", nil) 569 - return 570 - } 571 - 572 - client, err := rp.oauth.ServiceClient( 573 - r, 574 - oauth.WithService(f.Knot), 575 - oauth.WithLxm(tangled.RepoDeleteBranchNSID), 576 - oauth.WithDev(rp.config.Core.Dev), 577 - ) 578 - if err != nil { 579 - fail("Failed to connect to knotserver", nil) 580 - return 581 - } 582 - 583 - err = tangled.RepoDeleteBranch( 584 - r.Context(), 585 - client, 586 - &tangled.RepoDeleteBranch_Input{ 587 - Branch: branch, 588 - Repo: f.RepoAt().String(), 589 - }, 590 - ) 591 - if err := xrpcclient.HandleXrpcErr(err); err != nil { 592 - fail(fmt.Sprintf("Failed to delete branch: %s", err), err) 593 - return 594 - } 595 - l.Error("deleted branch from knot", "branch", branch, "repo", f.RepoAt()) 596 - 597 - rp.pages.HxRefresh(w) 598 - } 599 - 600 - func (rp *Repo) RepoBlob(w http.ResponseWriter, r *http.Request) { 601 - l := rp.logger.With("handler", "RepoBlob") 602 - 603 - f, err := rp.repoResolver.Resolve(r) 604 - if err != nil { 605 - l.Error("failed to get repo and knot", "err", err) 606 - return 607 - } 608 - 609 - ref := chi.URLParam(r, "ref") 610 - ref, _ = url.PathUnescape(ref) 611 - 612 - filePath := chi.URLParam(r, "*") 613 - filePath, _ = url.PathUnescape(filePath) 614 - 615 - scheme := "http" 616 - if !rp.config.Core.Dev { 617 - scheme = "https" 618 - } 619 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 620 - xrpcc := &indigoxrpc.Client{ 621 - Host: host, 622 - } 623 - 624 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 625 - resp, err := tangled.RepoBlob(r.Context(), xrpcc, filePath, false, ref, repo) 626 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 627 - l.Error("failed to call XRPC repo.blob", "err", xrpcerr) 628 - rp.pages.Error503(w) 629 - return 630 - } 631 - 632 - // Use XRPC response directly instead of converting to internal types 633 - 634 - var breadcrumbs [][]string 635 - breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))}) 636 - if filePath != "" { 637 - for idx, elem := range strings.Split(filePath, "/") { 638 - breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))}) 639 - } 640 - } 641 - 642 - showRendered := false 643 - renderToggle := false 644 - 645 - if markup.GetFormat(resp.Path) == markup.FormatMarkdown { 646 - renderToggle = true 647 - showRendered = r.URL.Query().Get("code") != "true" 648 - } 649 - 650 - var unsupported bool 651 - var isImage bool 652 - var isVideo bool 653 - var contentSrc string 654 - 655 - if resp.IsBinary != nil && *resp.IsBinary { 656 - ext := strings.ToLower(filepath.Ext(resp.Path)) 657 - switch ext { 658 - case ".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp": 659 - isImage = true 660 - case ".mp4", ".webm", ".ogg", ".mov", ".avi": 661 - isVideo = true 662 - default: 663 - unsupported = true 664 - } 665 - 666 - // fetch the raw binary content using sh.tangled.repo.blob xrpc 667 - repoName := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 668 - 669 - baseURL := &url.URL{ 670 - Scheme: scheme, 671 - Host: f.Knot, 672 - Path: "/xrpc/sh.tangled.repo.blob", 673 - } 674 - query := baseURL.Query() 675 - query.Set("repo", repoName) 676 - query.Set("ref", ref) 677 - query.Set("path", filePath) 678 - query.Set("raw", "true") 679 - baseURL.RawQuery = query.Encode() 680 - blobURL := baseURL.String() 681 - 682 - contentSrc = blobURL 683 - if !rp.config.Core.Dev { 684 - contentSrc = markup.GenerateCamoURL(rp.config.Camo.Host, rp.config.Camo.SharedSecret, blobURL) 685 - } 686 - } 687 - 688 - lines := 0 689 - if resp.IsBinary == nil || !*resp.IsBinary { 690 - lines = strings.Count(resp.Content, "\n") + 1 691 - } 692 - 693 - var sizeHint uint64 694 - if resp.Size != nil { 695 - sizeHint = uint64(*resp.Size) 696 - } else { 697 - sizeHint = uint64(len(resp.Content)) 698 - } 699 - 700 - user := rp.oauth.GetUser(r) 701 - 702 - // Determine if content is binary (dereference pointer) 703 - isBinary := false 704 - if resp.IsBinary != nil { 705 - isBinary = *resp.IsBinary 706 - } 707 - 708 - rp.pages.RepoBlob(w, pages.RepoBlobParams{ 709 - LoggedInUser: user, 710 - RepoInfo: f.RepoInfo(user), 711 - BreadCrumbs: breadcrumbs, 712 - ShowRendered: showRendered, 713 - RenderToggle: renderToggle, 714 - Unsupported: unsupported, 715 - IsImage: isImage, 716 - IsVideo: isVideo, 717 - ContentSrc: contentSrc, 718 - RepoBlob_Output: resp, 719 - Contents: resp.Content, 720 - Lines: lines, 721 - SizeHint: sizeHint, 722 - IsBinary: isBinary, 723 - }) 724 - } 725 - 726 - func (rp *Repo) RepoBlobRaw(w http.ResponseWriter, r *http.Request) { 727 - l := rp.logger.With("handler", "RepoBlobRaw") 728 - 729 - f, err := rp.repoResolver.Resolve(r) 730 - if err != nil { 731 - l.Error("failed to get repo and knot", "err", err) 732 - w.WriteHeader(http.StatusBadRequest) 733 - return 734 - } 735 - 736 - ref := chi.URLParam(r, "ref") 737 - ref, _ = url.PathUnescape(ref) 738 - 739 - filePath := chi.URLParam(r, "*") 740 - filePath, _ = url.PathUnescape(filePath) 741 - 742 - scheme := "http" 743 - if !rp.config.Core.Dev { 744 - scheme = "https" 745 - } 746 - 747 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Repo.Name) 748 - baseURL := &url.URL{ 749 - Scheme: scheme, 750 - Host: f.Knot, 751 - Path: "/xrpc/sh.tangled.repo.blob", 752 - } 753 - query := baseURL.Query() 754 - query.Set("repo", repo) 755 - query.Set("ref", ref) 756 - query.Set("path", filePath) 757 - query.Set("raw", "true") 758 - baseURL.RawQuery = query.Encode() 759 - blobURL := baseURL.String() 760 - 761 - req, err := http.NewRequest("GET", blobURL, nil) 762 - if err != nil { 763 - l.Error("failed to create request", "err", err) 764 - return 765 - } 766 - 767 - // forward the If-None-Match header 768 - if clientETag := r.Header.Get("If-None-Match"); clientETag != "" { 769 - req.Header.Set("If-None-Match", clientETag) 770 - } 771 - 772 - client := &http.Client{} 773 - resp, err := client.Do(req) 774 - if err != nil { 775 - l.Error("failed to reach knotserver", "err", err) 776 - rp.pages.Error503(w) 777 - return 778 - } 779 - defer resp.Body.Close() 780 - 781 - // forward 304 not modified 782 - if resp.StatusCode == http.StatusNotModified { 783 - w.WriteHeader(http.StatusNotModified) 784 - return 785 - } 786 - 787 - if resp.StatusCode != http.StatusOK { 788 - l.Error("knotserver returned non-OK status for raw blob", "url", blobURL, "statuscode", resp.StatusCode) 789 - w.WriteHeader(resp.StatusCode) 790 - _, _ = io.Copy(w, resp.Body) 791 - return 792 - } 793 - 794 - contentType := resp.Header.Get("Content-Type") 795 - body, err := io.ReadAll(resp.Body) 796 - if err != nil { 797 - l.Error("error reading response body from knotserver", "err", err) 798 - w.WriteHeader(http.StatusInternalServerError) 799 - return 800 - } 801 - 802 - if strings.HasPrefix(contentType, "text/") || isTextualMimeType(contentType) { 803 - // serve all textual content as text/plain 804 - w.Header().Set("Content-Type", "text/plain; charset=utf-8") 805 - w.Write(body) 806 - } else if strings.HasPrefix(contentType, "image/") || strings.HasPrefix(contentType, "video/") { 807 - // serve images and videos with their original content type 808 - w.Header().Set("Content-Type", contentType) 809 - w.Write(body) 810 - } else { 811 - w.WriteHeader(http.StatusUnsupportedMediaType) 812 - w.Write([]byte("unsupported content type")) 813 - return 814 - } 815 - } 816 - 817 81 // isTextualMimeType returns true if the MIME type represents textual content 818 - // that should be served as text/plain 819 - func isTextualMimeType(mimeType string) bool { 820 - textualTypes := []string{ 821 - "application/json", 822 - "application/xml", 823 - "application/yaml", 824 - "application/x-yaml", 825 - "application/toml", 826 - "application/javascript", 827 - "application/ecmascript", 828 - "message/", 829 - } 830 - 831 - return slices.Contains(textualTypes, mimeType) 832 - } 833 82 834 83 // modify the spindle configured for this repo 835 84 func (rp *Repo) EditSpindle(w http.ResponseWriter, r *http.Request) { ··· 1548 797 Rkey: rkey, 1549 798 SubjectDid: collaboratorIdent.DID, 1550 799 RepoAt: f.RepoAt(), 1551 - Created: createdAt, 1552 - }) 1553 - if err != nil { 1554 - fail("Failed to add collaborator.", err) 1555 - return 1556 - } 1557 - 1558 - err = tx.Commit() 1559 - if err != nil { 1560 - fail("Failed to add collaborator.", err) 1561 - return 1562 - } 1563 - 1564 - err = rp.enforcer.E.SavePolicy() 1565 - if err != nil { 1566 - fail("Failed to update collaborator permissions.", err) 1567 - return 1568 - } 1569 - 1570 - // clear aturi to when everything is successful 1571 - aturi = "" 1572 - 1573 - rp.pages.HxRefresh(w) 1574 - } 1575 - 1576 - func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 1577 - user := rp.oauth.GetUser(r) 1578 - l := rp.logger.With("handler", "DeleteRepo") 1579 - 1580 - noticeId := "operation-error" 1581 - f, err := rp.repoResolver.Resolve(r) 1582 - if err != nil { 1583 - l.Error("failed to get repo and knot", "err", err) 1584 - return 1585 - } 1586 - 1587 - // remove record from pds 1588 - atpClient, err := rp.oauth.AuthorizedClient(r) 1589 - if err != nil { 1590 - l.Error("failed to get authorized client", "err", err) 1591 - return 1592 - } 1593 - _, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{ 1594 - Collection: tangled.RepoNSID, 1595 - Repo: user.Did, 1596 - Rkey: f.Rkey, 1597 - }) 1598 - if err != nil { 1599 - l.Error("failed to delete record", "err", err) 1600 - rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.") 1601 - return 1602 - } 1603 - l.Info("removed repo record", "aturi", f.RepoAt().String()) 1604 - 1605 - client, err := rp.oauth.ServiceClient( 1606 - r, 1607 - oauth.WithService(f.Knot), 1608 - oauth.WithLxm(tangled.RepoDeleteNSID), 1609 - oauth.WithDev(rp.config.Core.Dev), 1610 - ) 1611 - if err != nil { 1612 - l.Error("failed to connect to knot server", "err", err) 1613 - return 1614 - } 1615 - 1616 - err = tangled.RepoDelete( 1617 - r.Context(), 1618 - client, 1619 - &tangled.RepoDelete_Input{ 1620 - Did: f.OwnerDid(), 1621 - Name: f.Name, 1622 - Rkey: f.Rkey, 1623 - }, 1624 - ) 1625 - if err := xrpcclient.HandleXrpcErr(err); err != nil { 1626 - rp.pages.Notice(w, noticeId, err.Error()) 1627 - return 1628 - } 1629 - l.Info("deleted repo from knot") 1630 - 1631 - tx, err := rp.db.BeginTx(r.Context(), nil) 1632 - if err != nil { 1633 - l.Error("failed to start tx") 1634 - w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 1635 - return 1636 - } 1637 - defer func() { 1638 - tx.Rollback() 1639 - err = rp.enforcer.E.LoadPolicy() 1640 - if err != nil { 1641 - l.Error("failed to rollback policies") 1642 - } 1643 - }() 1644 - 1645 - // remove collaborator RBAC 1646 - repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 1647 - if err != nil { 1648 - rp.pages.Notice(w, noticeId, "Failed to remove collaborators") 1649 - return 1650 - } 1651 - for _, c := range repoCollaborators { 1652 - did := c[0] 1653 - rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) 1654 - } 1655 - l.Info("removed collaborators") 1656 - 1657 - // remove repo RBAC 1658 - err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) 1659 - if err != nil { 1660 - rp.pages.Notice(w, noticeId, "Failed to update RBAC rules") 1661 - return 1662 - } 1663 - 1664 - // remove repo from db 1665 - err = db.RemoveRepo(tx, f.OwnerDid(), f.Name) 1666 - if err != nil { 1667 - rp.pages.Notice(w, noticeId, "Failed to update appview") 1668 - return 1669 - } 1670 - l.Info("removed repo from db") 1671 - 1672 - err = tx.Commit() 1673 - if err != nil { 1674 - l.Error("failed to commit changes", "err", err) 1675 - http.Error(w, err.Error(), http.StatusInternalServerError) 1676 - return 1677 - } 1678 - 1679 - err = rp.enforcer.E.SavePolicy() 1680 - if err != nil { 1681 - l.Error("failed to update ACLs", "err", err) 1682 - http.Error(w, err.Error(), http.StatusInternalServerError) 1683 - return 1684 - } 1685 - 1686 - rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid())) 1687 - } 1688 - 1689 - func (rp *Repo) EditBaseSettings(w http.ResponseWriter, r *http.Request) { 1690 - l := rp.logger.With("handler", "EditBaseSettings") 1691 - 1692 - noticeId := "repo-base-settings-error" 1693 - 1694 - f, err := rp.repoResolver.Resolve(r) 1695 - if err != nil { 1696 - l.Error("failed to get repo and knot", "err", err) 1697 - w.WriteHeader(http.StatusBadRequest) 1698 - return 1699 - } 1700 - 1701 - client, err := rp.oauth.AuthorizedClient(r) 1702 - if err != nil { 1703 - l.Error("failed to get client") 1704 - rp.pages.Notice(w, noticeId, "Failed to update repository information, try again later.") 1705 - return 1706 - } 1707 - 1708 - var ( 1709 - description = r.FormValue("description") 1710 - website = r.FormValue("website") 1711 - topicStr = r.FormValue("topics") 1712 - ) 1713 - 1714 - err = rp.validator.ValidateURI(website) 1715 - if err != nil { 1716 - l.Error("invalid uri", "err", err) 1717 - rp.pages.Notice(w, noticeId, err.Error()) 1718 - return 1719 - } 1720 - 1721 - topics, err := rp.validator.ValidateRepoTopicStr(topicStr) 1722 - if err != nil { 1723 - l.Error("invalid topics", "err", err) 1724 - rp.pages.Notice(w, noticeId, err.Error()) 1725 - return 1726 - } 1727 - l.Debug("got", "topicsStr", topicStr, "topics", topics) 1728 - 1729 - newRepo := f.Repo 1730 - newRepo.Description = description 1731 - newRepo.Website = website 1732 - newRepo.Topics = topics 1733 - record := newRepo.AsRecord() 1734 - 1735 - tx, err := rp.db.BeginTx(r.Context(), nil) 1736 - if err != nil { 1737 - l.Error("failed to begin transaction", "err", err) 1738 - rp.pages.Notice(w, noticeId, "Failed to save repository information.") 1739 - return 1740 - } 1741 - defer tx.Rollback() 1742 - 1743 - err = db.PutRepo(tx, newRepo) 1744 - if err != nil { 1745 - l.Error("failed to update repository", "err", err) 1746 - rp.pages.Notice(w, noticeId, "Failed to save repository information.") 1747 - return 1748 - } 1749 - 1750 - ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 1751 - if err != nil { 1752 - // failed to get record 1753 - l.Error("failed to get repo record", "err", err) 1754 - rp.pages.Notice(w, noticeId, "Failed to save repository information, no record found on PDS.") 1755 - return 1756 - } 1757 - _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 1758 - Collection: tangled.RepoNSID, 1759 - Repo: newRepo.Did, 1760 - Rkey: newRepo.Rkey, 1761 - SwapRecord: ex.Cid, 1762 - Record: &lexutil.LexiconTypeDecoder{ 1763 - Val: &record, 1764 - }, 800 + Created: createdAt, 1765 801 }) 1766 - 1767 802 if err != nil { 1768 - l.Error("failed to perferom update-repo query", "err", err) 1769 - // failed to get record 1770 - rp.pages.Notice(w, noticeId, "Failed to save repository information, unable to save to PDS.") 803 + fail("Failed to add collaborator.", err) 1771 804 return 1772 805 } 1773 806 1774 807 err = tx.Commit() 1775 808 if err != nil { 1776 - l.Error("failed to commit", "err", err) 809 + fail("Failed to add collaborator.", err) 810 + return 811 + } 812 + 813 + err = rp.enforcer.E.SavePolicy() 814 + if err != nil { 815 + fail("Failed to update collaborator permissions.", err) 816 + return 1777 817 } 1778 818 819 + // clear aturi to when everything is successful 820 + aturi = "" 821 + 1779 822 rp.pages.HxRefresh(w) 1780 823 } 1781 824 1782 - func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 1783 - l := rp.logger.With("handler", "SetDefaultBranch") 825 + func (rp *Repo) DeleteRepo(w http.ResponseWriter, r *http.Request) { 826 + user := rp.oauth.GetUser(r) 827 + l := rp.logger.With("handler", "DeleteRepo") 1784 828 829 + noticeId := "operation-error" 1785 830 f, err := rp.repoResolver.Resolve(r) 1786 831 if err != nil { 1787 832 l.Error("failed to get repo and knot", "err", err) 1788 833 return 1789 834 } 1790 835 1791 - noticeId := "operation-error" 1792 - branch := r.FormValue("branch") 1793 - if branch == "" { 1794 - http.Error(w, "malformed form", http.StatusBadRequest) 836 + // remove record from pds 837 + atpClient, err := rp.oauth.AuthorizedClient(r) 838 + if err != nil { 839 + l.Error("failed to get authorized client", "err", err) 840 + return 841 + } 842 + _, err = comatproto.RepoDeleteRecord(r.Context(), atpClient, &comatproto.RepoDeleteRecord_Input{ 843 + Collection: tangled.RepoNSID, 844 + Repo: user.Did, 845 + Rkey: f.Rkey, 846 + }) 847 + if err != nil { 848 + l.Error("failed to delete record", "err", err) 849 + rp.pages.Notice(w, noticeId, "Failed to delete repository from PDS.") 1795 850 return 1796 851 } 852 + l.Info("removed repo record", "aturi", f.RepoAt().String()) 1797 853 1798 854 client, err := rp.oauth.ServiceClient( 1799 855 r, 1800 856 oauth.WithService(f.Knot), 1801 - oauth.WithLxm(tangled.RepoSetDefaultBranchNSID), 857 + oauth.WithLxm(tangled.RepoDeleteNSID), 1802 858 oauth.WithDev(rp.config.Core.Dev), 1803 859 ) 1804 860 if err != nil { 1805 861 l.Error("failed to connect to knot server", "err", err) 1806 - rp.pages.Notice(w, noticeId, "Failed to connect to knot server.") 1807 862 return 1808 863 } 1809 864 1810 - xe := tangled.RepoSetDefaultBranch( 865 + err = tangled.RepoDelete( 1811 866 r.Context(), 1812 867 client, 1813 - &tangled.RepoSetDefaultBranch_Input{ 1814 - Repo: f.RepoAt().String(), 1815 - DefaultBranch: branch, 868 + &tangled.RepoDelete_Input{ 869 + Did: f.OwnerDid(), 870 + Name: f.Name, 871 + Rkey: f.Rkey, 1816 872 }, 1817 873 ) 1818 - if err := xrpcclient.HandleXrpcErr(xe); err != nil { 1819 - l.Error("xrpc failed", "err", xe) 874 + if err := xrpcclient.HandleXrpcErr(err); err != nil { 1820 875 rp.pages.Notice(w, noticeId, err.Error()) 1821 876 return 1822 877 } 878 + l.Info("deleted repo from knot") 1823 879 1824 - rp.pages.HxRefresh(w) 1825 - } 1826 - 1827 - func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { 1828 - user := rp.oauth.GetUser(r) 1829 - l := rp.logger.With("handler", "Secrets") 1830 - l = l.With("did", user.Did) 1831 - 1832 - f, err := rp.repoResolver.Resolve(r) 1833 - if err != nil { 1834 - l.Error("failed to get repo and knot", "err", err) 1835 - return 1836 - } 1837 - 1838 - if f.Spindle == "" { 1839 - l.Error("empty spindle cannot add/rm secret", "err", err) 1840 - return 1841 - } 1842 - 1843 - lxm := tangled.RepoAddSecretNSID 1844 - if r.Method == http.MethodDelete { 1845 - lxm = tangled.RepoRemoveSecretNSID 1846 - } 1847 - 1848 - spindleClient, err := rp.oauth.ServiceClient( 1849 - r, 1850 - oauth.WithService(f.Spindle), 1851 - oauth.WithLxm(lxm), 1852 - oauth.WithExp(60), 1853 - oauth.WithDev(rp.config.Core.Dev), 1854 - ) 880 + tx, err := rp.db.BeginTx(r.Context(), nil) 1855 881 if err != nil { 1856 - l.Error("failed to create spindle client", "err", err) 1857 - return 1858 - } 1859 - 1860 - key := r.FormValue("key") 1861 - if key == "" { 1862 - w.WriteHeader(http.StatusBadRequest) 882 + l.Error("failed to start tx") 883 + w.Write(fmt.Append(nil, "failed to add collaborator: ", err)) 1863 884 return 1864 885 } 1865 - 1866 - switch r.Method { 1867 - case http.MethodPut: 1868 - errorId := "add-secret-error" 1869 - 1870 - value := r.FormValue("value") 1871 - if value == "" { 1872 - w.WriteHeader(http.StatusBadRequest) 1873 - return 1874 - } 1875 - 1876 - err = tangled.RepoAddSecret( 1877 - r.Context(), 1878 - spindleClient, 1879 - &tangled.RepoAddSecret_Input{ 1880 - Repo: f.RepoAt().String(), 1881 - Key: key, 1882 - Value: value, 1883 - }, 1884 - ) 1885 - if err != nil { 1886 - l.Error("Failed to add secret.", "err", err) 1887 - rp.pages.Notice(w, errorId, "Failed to add secret.") 1888 - return 1889 - } 1890 - 1891 - case http.MethodDelete: 1892 - errorId := "operation-error" 1893 - 1894 - err = tangled.RepoRemoveSecret( 1895 - r.Context(), 1896 - spindleClient, 1897 - &tangled.RepoRemoveSecret_Input{ 1898 - Repo: f.RepoAt().String(), 1899 - Key: key, 1900 - }, 1901 - ) 886 + defer func() { 887 + tx.Rollback() 888 + err = rp.enforcer.E.LoadPolicy() 1902 889 if err != nil { 1903 - l.Error("Failed to delete secret.", "err", err) 1904 - rp.pages.Notice(w, errorId, "Failed to delete secret.") 1905 - return 890 + l.Error("failed to rollback policies") 1906 891 } 1907 - } 1908 - 1909 - rp.pages.HxRefresh(w) 1910 - } 1911 - 1912 - type tab = map[string]any 1913 - 1914 - var ( 1915 - // would be great to have ordered maps right about now 1916 - settingsTabs []tab = []tab{ 1917 - {"Name": "general", "Icon": "sliders-horizontal"}, 1918 - {"Name": "access", "Icon": "users"}, 1919 - {"Name": "pipelines", "Icon": "layers-2"}, 1920 - } 1921 - ) 1922 - 1923 - func (rp *Repo) RepoSettings(w http.ResponseWriter, r *http.Request) { 1924 - tabVal := r.URL.Query().Get("tab") 1925 - if tabVal == "" { 1926 - tabVal = "general" 1927 - } 1928 - 1929 - switch tabVal { 1930 - case "general": 1931 - rp.generalSettings(w, r) 1932 - 1933 - case "access": 1934 - rp.accessSettings(w, r) 1935 - 1936 - case "pipelines": 1937 - rp.pipelineSettings(w, r) 1938 - } 1939 - } 1940 - 1941 - func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) { 1942 - l := rp.logger.With("handler", "generalSettings") 1943 - 1944 - f, err := rp.repoResolver.Resolve(r) 1945 - user := rp.oauth.GetUser(r) 1946 - 1947 - scheme := "http" 1948 - if !rp.config.Core.Dev { 1949 - scheme = "https" 1950 - } 1951 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 1952 - xrpcc := &indigoxrpc.Client{ 1953 - Host: host, 1954 - } 892 + }() 1955 893 1956 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 1957 - xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 1958 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 1959 - l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 1960 - rp.pages.Error503(w) 894 + // remove collaborator RBAC 895 + repoCollaborators, err := rp.enforcer.E.GetImplicitUsersForResourceByDomain(f.DidSlashRepo(), f.Knot) 896 + if err != nil { 897 + rp.pages.Notice(w, noticeId, "Failed to remove collaborators") 1961 898 return 1962 899 } 1963 - 1964 - var result types.RepoBranchesResponse 1965 - if err := json.Unmarshal(xrpcBytes, &result); err != nil { 1966 - l.Error("failed to decode XRPC response", "err", err) 1967 - rp.pages.Error503(w) 1968 - return 900 + for _, c := range repoCollaborators { 901 + did := c[0] 902 + rp.enforcer.RemoveCollaborator(did, f.Knot, f.DidSlashRepo()) 1969 903 } 904 + l.Info("removed collaborators") 1970 905 1971 - defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs)) 906 + // remove repo RBAC 907 + err = rp.enforcer.RemoveRepo(f.OwnerDid(), f.Knot, f.DidSlashRepo()) 1972 908 if err != nil { 1973 - l.Error("failed to fetch labels", "err", err) 1974 - rp.pages.Error503(w) 909 + rp.pages.Notice(w, noticeId, "Failed to update RBAC rules") 1975 910 return 1976 911 } 1977 912 1978 - labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels)) 913 + // remove repo from db 914 + err = db.RemoveRepo(tx, f.OwnerDid(), f.Name) 1979 915 if err != nil { 1980 - l.Error("failed to fetch labels", "err", err) 1981 - rp.pages.Error503(w) 916 + rp.pages.Notice(w, noticeId, "Failed to update appview") 1982 917 return 1983 918 } 1984 - // remove default labels from the labels list, if present 1985 - defaultLabelMap := make(map[string]bool) 1986 - for _, dl := range defaultLabels { 1987 - defaultLabelMap[dl.AtUri().String()] = true 1988 - } 1989 - n := 0 1990 - for _, l := range labels { 1991 - if !defaultLabelMap[l.AtUri().String()] { 1992 - labels[n] = l 1993 - n++ 1994 - } 1995 - } 1996 - labels = labels[:n] 1997 - 1998 - subscribedLabels := make(map[string]struct{}) 1999 - for _, l := range f.Repo.Labels { 2000 - subscribedLabels[l] = struct{}{} 2001 - } 2002 - 2003 - // if there is atleast 1 unsubbed default label, show the "subscribe all" button, 2004 - // if all default labels are subbed, show the "unsubscribe all" button 2005 - shouldSubscribeAll := false 2006 - for _, dl := range defaultLabels { 2007 - if _, ok := subscribedLabels[dl.AtUri().String()]; !ok { 2008 - // one of the default labels is not subscribed to 2009 - shouldSubscribeAll = true 2010 - break 2011 - } 2012 - } 2013 - 2014 - rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{ 2015 - LoggedInUser: user, 2016 - RepoInfo: f.RepoInfo(user), 2017 - Branches: result.Branches, 2018 - Labels: labels, 2019 - DefaultLabels: defaultLabels, 2020 - SubscribedLabels: subscribedLabels, 2021 - ShouldSubscribeAll: shouldSubscribeAll, 2022 - Tabs: settingsTabs, 2023 - Tab: "general", 2024 - }) 2025 - } 2026 - 2027 - func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) { 2028 - l := rp.logger.With("handler", "accessSettings") 2029 - 2030 - f, err := rp.repoResolver.Resolve(r) 2031 - user := rp.oauth.GetUser(r) 919 + l.Info("removed repo from db") 2032 920 2033 - repoCollaborators, err := f.Collaborators(r.Context()) 921 + err = tx.Commit() 2034 922 if err != nil { 2035 - l.Error("failed to get collaborators", "err", err) 923 + l.Error("failed to commit changes", "err", err) 924 + http.Error(w, err.Error(), http.StatusInternalServerError) 925 + return 2036 926 } 2037 927 2038 - rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{ 2039 - LoggedInUser: user, 2040 - RepoInfo: f.RepoInfo(user), 2041 - Tabs: settingsTabs, 2042 - Tab: "access", 2043 - Collaborators: repoCollaborators, 2044 - }) 2045 - } 2046 - 2047 - func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) { 2048 - l := rp.logger.With("handler", "pipelineSettings") 2049 - 2050 - f, err := rp.repoResolver.Resolve(r) 2051 - user := rp.oauth.GetUser(r) 2052 - 2053 - // all spindles that the repo owner is a member of 2054 - spindles, err := rp.enforcer.GetSpindlesForUser(f.OwnerDid()) 928 + err = rp.enforcer.E.SavePolicy() 2055 929 if err != nil { 2056 - l.Error("failed to fetch spindles", "err", err) 930 + l.Error("failed to update ACLs", "err", err) 931 + http.Error(w, err.Error(), http.StatusInternalServerError) 2057 932 return 2058 933 } 2059 934 2060 - var secrets []*tangled.RepoListSecrets_Secret 2061 - if f.Spindle != "" { 2062 - if spindleClient, err := rp.oauth.ServiceClient( 2063 - r, 2064 - oauth.WithService(f.Spindle), 2065 - oauth.WithLxm(tangled.RepoListSecretsNSID), 2066 - oauth.WithExp(60), 2067 - oauth.WithDev(rp.config.Core.Dev), 2068 - ); err != nil { 2069 - l.Error("failed to create spindle client", "err", err) 2070 - } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 2071 - l.Error("failed to fetch secrets", "err", err) 2072 - } else { 2073 - secrets = resp.Secrets 2074 - } 2075 - } 2076 - 2077 - slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int { 2078 - return strings.Compare(a.Key, b.Key) 2079 - }) 2080 - 2081 - var dids []string 2082 - for _, s := range secrets { 2083 - dids = append(dids, s.CreatedBy) 2084 - } 2085 - resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids) 2086 - 2087 - // convert to a more manageable form 2088 - var niceSecret []map[string]any 2089 - for id, s := range secrets { 2090 - when, _ := time.Parse(time.RFC3339, s.CreatedAt) 2091 - niceSecret = append(niceSecret, map[string]any{ 2092 - "Id": id, 2093 - "Key": s.Key, 2094 - "CreatedAt": when, 2095 - "CreatedBy": resolvedIdents[id].Handle.String(), 2096 - }) 2097 - } 2098 - 2099 - rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{ 2100 - LoggedInUser: user, 2101 - RepoInfo: f.RepoInfo(user), 2102 - Tabs: settingsTabs, 2103 - Tab: "pipelines", 2104 - Spindles: spindles, 2105 - CurrentSpindle: f.Spindle, 2106 - Secrets: niceSecret, 2107 - }) 935 + rp.pages.HxRedirect(w, fmt.Sprintf("/%s", f.OwnerDid())) 2108 936 } 2109 937 2110 938 func (rp *Repo) SyncRepoFork(w http.ResponseWriter, r *http.Request) { ··· 2388 1216 }) 2389 1217 return err 2390 1218 } 2391 - 2392 - func (rp *Repo) RepoCompareNew(w http.ResponseWriter, r *http.Request) { 2393 - l := rp.logger.With("handler", "RepoCompareNew") 2394 - 2395 - user := rp.oauth.GetUser(r) 2396 - f, err := rp.repoResolver.Resolve(r) 2397 - if err != nil { 2398 - l.Error("failed to get repo and knot", "err", err) 2399 - return 2400 - } 2401 - 2402 - scheme := "http" 2403 - if !rp.config.Core.Dev { 2404 - scheme = "https" 2405 - } 2406 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 2407 - xrpcc := &indigoxrpc.Client{ 2408 - Host: host, 2409 - } 2410 - 2411 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 2412 - branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 2413 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2414 - l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 2415 - rp.pages.Error503(w) 2416 - return 2417 - } 2418 - 2419 - var branchResult types.RepoBranchesResponse 2420 - if err := json.Unmarshal(branchBytes, &branchResult); err != nil { 2421 - l.Error("failed to decode XRPC branches response", "err", err) 2422 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2423 - return 2424 - } 2425 - branches := branchResult.Branches 2426 - 2427 - sortBranches(branches) 2428 - 2429 - var defaultBranch string 2430 - for _, b := range branches { 2431 - if b.IsDefault { 2432 - defaultBranch = b.Name 2433 - } 2434 - } 2435 - 2436 - base := defaultBranch 2437 - head := defaultBranch 2438 - 2439 - params := r.URL.Query() 2440 - queryBase := params.Get("base") 2441 - queryHead := params.Get("head") 2442 - if queryBase != "" { 2443 - base = queryBase 2444 - } 2445 - if queryHead != "" { 2446 - head = queryHead 2447 - } 2448 - 2449 - tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 2450 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2451 - l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 2452 - rp.pages.Error503(w) 2453 - return 2454 - } 2455 - 2456 - var tags types.RepoTagsResponse 2457 - if err := json.Unmarshal(tagBytes, &tags); err != nil { 2458 - l.Error("failed to decode XRPC tags response", "err", err) 2459 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2460 - return 2461 - } 2462 - 2463 - repoinfo := f.RepoInfo(user) 2464 - 2465 - rp.pages.RepoCompareNew(w, pages.RepoCompareNewParams{ 2466 - LoggedInUser: user, 2467 - RepoInfo: repoinfo, 2468 - Branches: branches, 2469 - Tags: tags.Tags, 2470 - Base: base, 2471 - Head: head, 2472 - }) 2473 - } 2474 - 2475 - func (rp *Repo) RepoCompare(w http.ResponseWriter, r *http.Request) { 2476 - l := rp.logger.With("handler", "RepoCompare") 2477 - 2478 - user := rp.oauth.GetUser(r) 2479 - f, err := rp.repoResolver.Resolve(r) 2480 - if err != nil { 2481 - l.Error("failed to get repo and knot", "err", err) 2482 - return 2483 - } 2484 - 2485 - var diffOpts types.DiffOpts 2486 - if d := r.URL.Query().Get("diff"); d == "split" { 2487 - diffOpts.Split = true 2488 - } 2489 - 2490 - // if user is navigating to one of 2491 - // /compare/{base}/{head} 2492 - // /compare/{base}...{head} 2493 - base := chi.URLParam(r, "base") 2494 - head := chi.URLParam(r, "head") 2495 - if base == "" && head == "" { 2496 - rest := chi.URLParam(r, "*") // master...feature/xyz 2497 - parts := strings.SplitN(rest, "...", 2) 2498 - if len(parts) == 2 { 2499 - base = parts[0] 2500 - head = parts[1] 2501 - } 2502 - } 2503 - 2504 - base, _ = url.PathUnescape(base) 2505 - head, _ = url.PathUnescape(head) 2506 - 2507 - if base == "" || head == "" { 2508 - l.Error("invalid comparison") 2509 - rp.pages.Error404(w) 2510 - return 2511 - } 2512 - 2513 - scheme := "http" 2514 - if !rp.config.Core.Dev { 2515 - scheme = "https" 2516 - } 2517 - host := fmt.Sprintf("%s://%s", scheme, f.Knot) 2518 - xrpcc := &indigoxrpc.Client{ 2519 - Host: host, 2520 - } 2521 - 2522 - repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 2523 - 2524 - branchBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 2525 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2526 - l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 2527 - rp.pages.Error503(w) 2528 - return 2529 - } 2530 - 2531 - var branches types.RepoBranchesResponse 2532 - if err := json.Unmarshal(branchBytes, &branches); err != nil { 2533 - l.Error("failed to decode XRPC branches response", "err", err) 2534 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2535 - return 2536 - } 2537 - 2538 - tagBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 2539 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2540 - l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 2541 - rp.pages.Error503(w) 2542 - return 2543 - } 2544 - 2545 - var tags types.RepoTagsResponse 2546 - if err := json.Unmarshal(tagBytes, &tags); err != nil { 2547 - l.Error("failed to decode XRPC tags response", "err", err) 2548 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2549 - return 2550 - } 2551 - 2552 - compareBytes, err := tangled.RepoCompare(r.Context(), xrpcc, repo, base, head) 2553 - if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 2554 - l.Error("failed to call XRPC repo.compare", "err", xrpcerr) 2555 - rp.pages.Error503(w) 2556 - return 2557 - } 2558 - 2559 - var formatPatch types.RepoFormatPatchResponse 2560 - if err := json.Unmarshal(compareBytes, &formatPatch); err != nil { 2561 - l.Error("failed to decode XRPC compare response", "err", err) 2562 - rp.pages.Notice(w, "compare-error", "Failed to produce comparison. Try again later.") 2563 - return 2564 - } 2565 - 2566 - var diff types.NiceDiff 2567 - if formatPatch.CombinedPatchRaw != "" { 2568 - diff = patchutil.AsNiceDiff(formatPatch.CombinedPatchRaw, base) 2569 - } else { 2570 - diff = patchutil.AsNiceDiff(formatPatch.FormatPatchRaw, base) 2571 - } 2572 - 2573 - repoinfo := f.RepoInfo(user) 2574 - 2575 - rp.pages.RepoCompare(w, pages.RepoCompareParams{ 2576 - LoggedInUser: user, 2577 - RepoInfo: repoinfo, 2578 - Branches: branches.Branches, 2579 - Tags: tags.Tags, 2580 - Base: base, 2581 - Head: head, 2582 - Diff: &diff, 2583 - DiffOpts: diffOpts, 2584 - }) 2585 - 2586 - }
+14 -14
appview/repo/router.go
··· 9 9 10 10 func (rp *Repo) Router(mw *middleware.Middleware) http.Handler { 11 11 r := chi.NewRouter() 12 - r.Get("/", rp.RepoIndex) 13 - r.Get("/opengraph", rp.RepoOpenGraphSummary) 14 - r.Get("/feed.atom", rp.RepoAtomFeed) 15 - r.Get("/commits/{ref}", rp.RepoLog) 12 + r.Get("/", rp.Index) 13 + r.Get("/opengraph", rp.Opengraph) 14 + r.Get("/feed.atom", rp.AtomFeed) 15 + r.Get("/commits/{ref}", rp.Log) 16 16 r.Route("/tree/{ref}", func(r chi.Router) { 17 - r.Get("/", rp.RepoIndex) 18 - r.Get("/*", rp.RepoTree) 17 + r.Get("/", rp.Index) 18 + r.Get("/*", rp.Tree) 19 19 }) 20 - r.Get("/commit/{ref}", rp.RepoCommit) 21 - r.Get("/branches", rp.RepoBranches) 20 + r.Get("/commit/{ref}", rp.Commit) 21 + r.Get("/branches", rp.Branches) 22 22 r.Delete("/branches", rp.DeleteBranch) 23 23 r.Route("/tags", func(r chi.Router) { 24 - r.Get("/", rp.RepoTags) 24 + r.Get("/", rp.Tags) 25 25 r.Route("/{tag}", func(r chi.Router) { 26 26 r.Get("/download/{file}", rp.DownloadArtifact) 27 27 ··· 37 37 }) 38 38 }) 39 39 }) 40 - r.Get("/blob/{ref}/*", rp.RepoBlob) 40 + r.Get("/blob/{ref}/*", rp.Blob) 41 41 r.Get("/raw/{ref}/*", rp.RepoBlobRaw) 42 42 43 43 // intentionally doesn't use /* as this isn't ··· 54 54 }) 55 55 56 56 r.Route("/compare", func(r chi.Router) { 57 - r.Get("/", rp.RepoCompareNew) // start an new comparison 57 + r.Get("/", rp.CompareNew) // start an new comparison 58 58 59 59 // we have to wildcard here since we want to support GitHub's compare syntax 60 60 // /compare/{ref1}...{ref2} 61 61 // for example: 62 62 // /compare/master...some/feature 63 63 // /compare/master...example.com:another/feature <- this is a fork 64 - r.Get("/{base}/{head}", rp.RepoCompare) 65 - r.Get("/*", rp.RepoCompare) 64 + r.Get("/{base}/{head}", rp.Compare) 65 + r.Get("/*", rp.Compare) 66 66 }) 67 67 68 68 // label panel in issues/pulls/discussions/tasks ··· 75 75 r.Group(func(r chi.Router) { 76 76 r.Use(middleware.AuthMiddleware(rp.oauth)) 77 77 r.With(mw.RepoPermissionMiddleware("repo:settings")).Route("/settings", func(r chi.Router) { 78 - r.Get("/", rp.RepoSettings) 78 + r.Get("/", rp.Settings) 79 79 r.With(mw.RepoPermissionMiddleware("repo:owner")).Put("/base", rp.EditBaseSettings) 80 80 r.With(mw.RepoPermissionMiddleware("repo:owner")).Post("/spindle", rp.EditSpindle) 81 81 r.With(mw.RepoPermissionMiddleware("repo:owner")).Put("/label", rp.AddLabelDef)
+442
appview/repo/settings.go
··· 1 + package repo 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + "slices" 8 + "strings" 9 + "time" 10 + 11 + "tangled.org/core/api/tangled" 12 + "tangled.org/core/appview/db" 13 + "tangled.org/core/appview/oauth" 14 + "tangled.org/core/appview/pages" 15 + xrpcclient "tangled.org/core/appview/xrpcclient" 16 + "tangled.org/core/types" 17 + 18 + comatproto "github.com/bluesky-social/indigo/api/atproto" 19 + lexutil "github.com/bluesky-social/indigo/lex/util" 20 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 21 + ) 22 + 23 + type tab = map[string]any 24 + 25 + var ( 26 + // would be great to have ordered maps right about now 27 + settingsTabs []tab = []tab{ 28 + {"Name": "general", "Icon": "sliders-horizontal"}, 29 + {"Name": "access", "Icon": "users"}, 30 + {"Name": "pipelines", "Icon": "layers-2"}, 31 + } 32 + ) 33 + 34 + func (rp *Repo) SetDefaultBranch(w http.ResponseWriter, r *http.Request) { 35 + l := rp.logger.With("handler", "SetDefaultBranch") 36 + 37 + f, err := rp.repoResolver.Resolve(r) 38 + if err != nil { 39 + l.Error("failed to get repo and knot", "err", err) 40 + return 41 + } 42 + 43 + noticeId := "operation-error" 44 + branch := r.FormValue("branch") 45 + if branch == "" { 46 + http.Error(w, "malformed form", http.StatusBadRequest) 47 + return 48 + } 49 + 50 + client, err := rp.oauth.ServiceClient( 51 + r, 52 + oauth.WithService(f.Knot), 53 + oauth.WithLxm(tangled.RepoSetDefaultBranchNSID), 54 + oauth.WithDev(rp.config.Core.Dev), 55 + ) 56 + if err != nil { 57 + l.Error("failed to connect to knot server", "err", err) 58 + rp.pages.Notice(w, noticeId, "Failed to connect to knot server.") 59 + return 60 + } 61 + 62 + xe := tangled.RepoSetDefaultBranch( 63 + r.Context(), 64 + client, 65 + &tangled.RepoSetDefaultBranch_Input{ 66 + Repo: f.RepoAt().String(), 67 + DefaultBranch: branch, 68 + }, 69 + ) 70 + if err := xrpcclient.HandleXrpcErr(xe); err != nil { 71 + l.Error("xrpc failed", "err", xe) 72 + rp.pages.Notice(w, noticeId, err.Error()) 73 + return 74 + } 75 + 76 + rp.pages.HxRefresh(w) 77 + } 78 + 79 + func (rp *Repo) Secrets(w http.ResponseWriter, r *http.Request) { 80 + user := rp.oauth.GetUser(r) 81 + l := rp.logger.With("handler", "Secrets") 82 + l = l.With("did", user.Did) 83 + 84 + f, err := rp.repoResolver.Resolve(r) 85 + if err != nil { 86 + l.Error("failed to get repo and knot", "err", err) 87 + return 88 + } 89 + 90 + if f.Spindle == "" { 91 + l.Error("empty spindle cannot add/rm secret", "err", err) 92 + return 93 + } 94 + 95 + lxm := tangled.RepoAddSecretNSID 96 + if r.Method == http.MethodDelete { 97 + lxm = tangled.RepoRemoveSecretNSID 98 + } 99 + 100 + spindleClient, err := rp.oauth.ServiceClient( 101 + r, 102 + oauth.WithService(f.Spindle), 103 + oauth.WithLxm(lxm), 104 + oauth.WithExp(60), 105 + oauth.WithDev(rp.config.Core.Dev), 106 + ) 107 + if err != nil { 108 + l.Error("failed to create spindle client", "err", err) 109 + return 110 + } 111 + 112 + key := r.FormValue("key") 113 + if key == "" { 114 + w.WriteHeader(http.StatusBadRequest) 115 + return 116 + } 117 + 118 + switch r.Method { 119 + case http.MethodPut: 120 + errorId := "add-secret-error" 121 + 122 + value := r.FormValue("value") 123 + if value == "" { 124 + w.WriteHeader(http.StatusBadRequest) 125 + return 126 + } 127 + 128 + err = tangled.RepoAddSecret( 129 + r.Context(), 130 + spindleClient, 131 + &tangled.RepoAddSecret_Input{ 132 + Repo: f.RepoAt().String(), 133 + Key: key, 134 + Value: value, 135 + }, 136 + ) 137 + if err != nil { 138 + l.Error("Failed to add secret.", "err", err) 139 + rp.pages.Notice(w, errorId, "Failed to add secret.") 140 + return 141 + } 142 + 143 + case http.MethodDelete: 144 + errorId := "operation-error" 145 + 146 + err = tangled.RepoRemoveSecret( 147 + r.Context(), 148 + spindleClient, 149 + &tangled.RepoRemoveSecret_Input{ 150 + Repo: f.RepoAt().String(), 151 + Key: key, 152 + }, 153 + ) 154 + if err != nil { 155 + l.Error("Failed to delete secret.", "err", err) 156 + rp.pages.Notice(w, errorId, "Failed to delete secret.") 157 + return 158 + } 159 + } 160 + 161 + rp.pages.HxRefresh(w) 162 + } 163 + 164 + func (rp *Repo) Settings(w http.ResponseWriter, r *http.Request) { 165 + tabVal := r.URL.Query().Get("tab") 166 + if tabVal == "" { 167 + tabVal = "general" 168 + } 169 + 170 + switch tabVal { 171 + case "general": 172 + rp.generalSettings(w, r) 173 + 174 + case "access": 175 + rp.accessSettings(w, r) 176 + 177 + case "pipelines": 178 + rp.pipelineSettings(w, r) 179 + } 180 + } 181 + 182 + func (rp *Repo) generalSettings(w http.ResponseWriter, r *http.Request) { 183 + l := rp.logger.With("handler", "generalSettings") 184 + 185 + f, err := rp.repoResolver.Resolve(r) 186 + user := rp.oauth.GetUser(r) 187 + 188 + scheme := "http" 189 + if !rp.config.Core.Dev { 190 + scheme = "https" 191 + } 192 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 193 + xrpcc := &indigoxrpc.Client{ 194 + Host: host, 195 + } 196 + 197 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 198 + xrpcBytes, err := tangled.RepoBranches(r.Context(), xrpcc, "", 0, repo) 199 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 200 + l.Error("failed to call XRPC repo.branches", "err", xrpcerr) 201 + rp.pages.Error503(w) 202 + return 203 + } 204 + 205 + var result types.RepoBranchesResponse 206 + if err := json.Unmarshal(xrpcBytes, &result); err != nil { 207 + l.Error("failed to decode XRPC response", "err", err) 208 + rp.pages.Error503(w) 209 + return 210 + } 211 + 212 + defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs)) 213 + if err != nil { 214 + l.Error("failed to fetch labels", "err", err) 215 + rp.pages.Error503(w) 216 + return 217 + } 218 + 219 + labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Repo.Labels)) 220 + if err != nil { 221 + l.Error("failed to fetch labels", "err", err) 222 + rp.pages.Error503(w) 223 + return 224 + } 225 + // remove default labels from the labels list, if present 226 + defaultLabelMap := make(map[string]bool) 227 + for _, dl := range defaultLabels { 228 + defaultLabelMap[dl.AtUri().String()] = true 229 + } 230 + n := 0 231 + for _, l := range labels { 232 + if !defaultLabelMap[l.AtUri().String()] { 233 + labels[n] = l 234 + n++ 235 + } 236 + } 237 + labels = labels[:n] 238 + 239 + subscribedLabels := make(map[string]struct{}) 240 + for _, l := range f.Repo.Labels { 241 + subscribedLabels[l] = struct{}{} 242 + } 243 + 244 + // if there is atleast 1 unsubbed default label, show the "subscribe all" button, 245 + // if all default labels are subbed, show the "unsubscribe all" button 246 + shouldSubscribeAll := false 247 + for _, dl := range defaultLabels { 248 + if _, ok := subscribedLabels[dl.AtUri().String()]; !ok { 249 + // one of the default labels is not subscribed to 250 + shouldSubscribeAll = true 251 + break 252 + } 253 + } 254 + 255 + rp.pages.RepoGeneralSettings(w, pages.RepoGeneralSettingsParams{ 256 + LoggedInUser: user, 257 + RepoInfo: f.RepoInfo(user), 258 + Branches: result.Branches, 259 + Labels: labels, 260 + DefaultLabels: defaultLabels, 261 + SubscribedLabels: subscribedLabels, 262 + ShouldSubscribeAll: shouldSubscribeAll, 263 + Tabs: settingsTabs, 264 + Tab: "general", 265 + }) 266 + } 267 + 268 + func (rp *Repo) accessSettings(w http.ResponseWriter, r *http.Request) { 269 + l := rp.logger.With("handler", "accessSettings") 270 + 271 + f, err := rp.repoResolver.Resolve(r) 272 + user := rp.oauth.GetUser(r) 273 + 274 + repoCollaborators, err := f.Collaborators(r.Context()) 275 + if err != nil { 276 + l.Error("failed to get collaborators", "err", err) 277 + } 278 + 279 + rp.pages.RepoAccessSettings(w, pages.RepoAccessSettingsParams{ 280 + LoggedInUser: user, 281 + RepoInfo: f.RepoInfo(user), 282 + Tabs: settingsTabs, 283 + Tab: "access", 284 + Collaborators: repoCollaborators, 285 + }) 286 + } 287 + 288 + func (rp *Repo) pipelineSettings(w http.ResponseWriter, r *http.Request) { 289 + l := rp.logger.With("handler", "pipelineSettings") 290 + 291 + f, err := rp.repoResolver.Resolve(r) 292 + user := rp.oauth.GetUser(r) 293 + 294 + // all spindles that the repo owner is a member of 295 + spindles, err := rp.enforcer.GetSpindlesForUser(f.OwnerDid()) 296 + if err != nil { 297 + l.Error("failed to fetch spindles", "err", err) 298 + return 299 + } 300 + 301 + var secrets []*tangled.RepoListSecrets_Secret 302 + if f.Spindle != "" { 303 + if spindleClient, err := rp.oauth.ServiceClient( 304 + r, 305 + oauth.WithService(f.Spindle), 306 + oauth.WithLxm(tangled.RepoListSecretsNSID), 307 + oauth.WithExp(60), 308 + oauth.WithDev(rp.config.Core.Dev), 309 + ); err != nil { 310 + l.Error("failed to create spindle client", "err", err) 311 + } else if resp, err := tangled.RepoListSecrets(r.Context(), spindleClient, f.RepoAt().String()); err != nil { 312 + l.Error("failed to fetch secrets", "err", err) 313 + } else { 314 + secrets = resp.Secrets 315 + } 316 + } 317 + 318 + slices.SortFunc(secrets, func(a, b *tangled.RepoListSecrets_Secret) int { 319 + return strings.Compare(a.Key, b.Key) 320 + }) 321 + 322 + var dids []string 323 + for _, s := range secrets { 324 + dids = append(dids, s.CreatedBy) 325 + } 326 + resolvedIdents := rp.idResolver.ResolveIdents(r.Context(), dids) 327 + 328 + // convert to a more manageable form 329 + var niceSecret []map[string]any 330 + for id, s := range secrets { 331 + when, _ := time.Parse(time.RFC3339, s.CreatedAt) 332 + niceSecret = append(niceSecret, map[string]any{ 333 + "Id": id, 334 + "Key": s.Key, 335 + "CreatedAt": when, 336 + "CreatedBy": resolvedIdents[id].Handle.String(), 337 + }) 338 + } 339 + 340 + rp.pages.RepoPipelineSettings(w, pages.RepoPipelineSettingsParams{ 341 + LoggedInUser: user, 342 + RepoInfo: f.RepoInfo(user), 343 + Tabs: settingsTabs, 344 + Tab: "pipelines", 345 + Spindles: spindles, 346 + CurrentSpindle: f.Spindle, 347 + Secrets: niceSecret, 348 + }) 349 + } 350 + 351 + func (rp *Repo) EditBaseSettings(w http.ResponseWriter, r *http.Request) { 352 + l := rp.logger.With("handler", "EditBaseSettings") 353 + 354 + noticeId := "repo-base-settings-error" 355 + 356 + f, err := rp.repoResolver.Resolve(r) 357 + if err != nil { 358 + l.Error("failed to get repo and knot", "err", err) 359 + w.WriteHeader(http.StatusBadRequest) 360 + return 361 + } 362 + 363 + client, err := rp.oauth.AuthorizedClient(r) 364 + if err != nil { 365 + l.Error("failed to get client") 366 + rp.pages.Notice(w, noticeId, "Failed to update repository information, try again later.") 367 + return 368 + } 369 + 370 + var ( 371 + description = r.FormValue("description") 372 + website = r.FormValue("website") 373 + topicStr = r.FormValue("topics") 374 + ) 375 + 376 + err = rp.validator.ValidateURI(website) 377 + if err != nil { 378 + l.Error("invalid uri", "err", err) 379 + rp.pages.Notice(w, noticeId, err.Error()) 380 + return 381 + } 382 + 383 + topics, err := rp.validator.ValidateRepoTopicStr(topicStr) 384 + if err != nil { 385 + l.Error("invalid topics", "err", err) 386 + rp.pages.Notice(w, noticeId, err.Error()) 387 + return 388 + } 389 + l.Debug("got", "topicsStr", topicStr, "topics", topics) 390 + 391 + newRepo := f.Repo 392 + newRepo.Description = description 393 + newRepo.Website = website 394 + newRepo.Topics = topics 395 + record := newRepo.AsRecord() 396 + 397 + tx, err := rp.db.BeginTx(r.Context(), nil) 398 + if err != nil { 399 + l.Error("failed to begin transaction", "err", err) 400 + rp.pages.Notice(w, noticeId, "Failed to save repository information.") 401 + return 402 + } 403 + defer tx.Rollback() 404 + 405 + err = db.PutRepo(tx, newRepo) 406 + if err != nil { 407 + l.Error("failed to update repository", "err", err) 408 + rp.pages.Notice(w, noticeId, "Failed to save repository information.") 409 + return 410 + } 411 + 412 + ex, err := comatproto.RepoGetRecord(r.Context(), client, "", tangled.RepoNSID, newRepo.Did, newRepo.Rkey) 413 + if err != nil { 414 + // failed to get record 415 + l.Error("failed to get repo record", "err", err) 416 + rp.pages.Notice(w, noticeId, "Failed to save repository information, no record found on PDS.") 417 + return 418 + } 419 + _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 420 + Collection: tangled.RepoNSID, 421 + Repo: newRepo.Did, 422 + Rkey: newRepo.Rkey, 423 + SwapRecord: ex.Cid, 424 + Record: &lexutil.LexiconTypeDecoder{ 425 + Val: &record, 426 + }, 427 + }) 428 + 429 + if err != nil { 430 + l.Error("failed to perferom update-repo query", "err", err) 431 + // failed to get record 432 + rp.pages.Notice(w, noticeId, "Failed to save repository information, unable to save to PDS.") 433 + return 434 + } 435 + 436 + err = tx.Commit() 437 + if err != nil { 438 + l.Error("failed to commit", "err", err) 439 + } 440 + 441 + rp.pages.HxRefresh(w) 442 + }
+79
appview/repo/tags.go
··· 1 + package repo 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "net/http" 7 + 8 + "tangled.org/core/api/tangled" 9 + "tangled.org/core/appview/db" 10 + "tangled.org/core/appview/models" 11 + "tangled.org/core/appview/pages" 12 + xrpcclient "tangled.org/core/appview/xrpcclient" 13 + "tangled.org/core/types" 14 + 15 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 16 + "github.com/go-git/go-git/v5/plumbing" 17 + ) 18 + 19 + func (rp *Repo) Tags(w http.ResponseWriter, r *http.Request) { 20 + l := rp.logger.With("handler", "RepoTags") 21 + f, err := rp.repoResolver.Resolve(r) 22 + if err != nil { 23 + l.Error("failed to get repo and knot", "err", err) 24 + return 25 + } 26 + scheme := "http" 27 + if !rp.config.Core.Dev { 28 + scheme = "https" 29 + } 30 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 31 + xrpcc := &indigoxrpc.Client{ 32 + Host: host, 33 + } 34 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 35 + xrpcBytes, err := tangled.RepoTags(r.Context(), xrpcc, "", 0, repo) 36 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 37 + l.Error("failed to call XRPC repo.tags", "err", xrpcerr) 38 + rp.pages.Error503(w) 39 + return 40 + } 41 + var result types.RepoTagsResponse 42 + if err := json.Unmarshal(xrpcBytes, &result); err != nil { 43 + l.Error("failed to decode XRPC response", "err", err) 44 + rp.pages.Error503(w) 45 + return 46 + } 47 + artifacts, err := db.GetArtifact(rp.db, db.FilterEq("repo_at", f.RepoAt())) 48 + if err != nil { 49 + l.Error("failed grab artifacts", "err", err) 50 + return 51 + } 52 + // convert artifacts to map for easy UI building 53 + artifactMap := make(map[plumbing.Hash][]models.Artifact) 54 + for _, a := range artifacts { 55 + artifactMap[a.Tag] = append(artifactMap[a.Tag], a) 56 + } 57 + var danglingArtifacts []models.Artifact 58 + for _, a := range artifacts { 59 + found := false 60 + for _, t := range result.Tags { 61 + if t.Tag != nil { 62 + if t.Tag.Hash == a.Tag { 63 + found = true 64 + } 65 + } 66 + } 67 + if !found { 68 + danglingArtifacts = append(danglingArtifacts, a) 69 + } 70 + } 71 + user := rp.oauth.GetUser(r) 72 + rp.pages.RepoTags(w, pages.RepoTagsParams{ 73 + LoggedInUser: user, 74 + RepoInfo: f.RepoInfo(user), 75 + RepoTagsResponse: result, 76 + ArtifactMap: artifactMap, 77 + DanglingArtifacts: danglingArtifacts, 78 + }) 79 + }
+107
appview/repo/tree.go
··· 1 + package repo 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "net/url" 7 + "strings" 8 + "time" 9 + 10 + "tangled.org/core/api/tangled" 11 + "tangled.org/core/appview/pages" 12 + xrpcclient "tangled.org/core/appview/xrpcclient" 13 + "tangled.org/core/types" 14 + 15 + indigoxrpc "github.com/bluesky-social/indigo/xrpc" 16 + "github.com/go-chi/chi/v5" 17 + "github.com/go-git/go-git/v5/plumbing" 18 + ) 19 + 20 + func (rp *Repo) Tree(w http.ResponseWriter, r *http.Request) { 21 + l := rp.logger.With("handler", "RepoTree") 22 + f, err := rp.repoResolver.Resolve(r) 23 + if err != nil { 24 + l.Error("failed to fully resolve repo", "err", err) 25 + return 26 + } 27 + ref := chi.URLParam(r, "ref") 28 + ref, _ = url.PathUnescape(ref) 29 + // if the tree path has a trailing slash, let's strip it 30 + // so we don't 404 31 + treePath := chi.URLParam(r, "*") 32 + treePath, _ = url.PathUnescape(treePath) 33 + treePath = strings.TrimSuffix(treePath, "/") 34 + scheme := "http" 35 + if !rp.config.Core.Dev { 36 + scheme = "https" 37 + } 38 + host := fmt.Sprintf("%s://%s", scheme, f.Knot) 39 + xrpcc := &indigoxrpc.Client{ 40 + Host: host, 41 + } 42 + repo := fmt.Sprintf("%s/%s", f.OwnerDid(), f.Name) 43 + xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo) 44 + if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 45 + l.Error("failed to call XRPC repo.tree", "err", xrpcerr) 46 + rp.pages.Error503(w) 47 + return 48 + } 49 + // Convert XRPC response to internal types.RepoTreeResponse 50 + files := make([]types.NiceTree, len(xrpcResp.Files)) 51 + for i, xrpcFile := range xrpcResp.Files { 52 + file := types.NiceTree{ 53 + Name: xrpcFile.Name, 54 + Mode: xrpcFile.Mode, 55 + Size: int64(xrpcFile.Size), 56 + IsFile: xrpcFile.Is_file, 57 + IsSubtree: xrpcFile.Is_subtree, 58 + } 59 + // Convert last commit info if present 60 + if xrpcFile.Last_commit != nil { 61 + commitWhen, _ := time.Parse(time.RFC3339, xrpcFile.Last_commit.When) 62 + file.LastCommit = &types.LastCommitInfo{ 63 + Hash: plumbing.NewHash(xrpcFile.Last_commit.Hash), 64 + Message: xrpcFile.Last_commit.Message, 65 + When: commitWhen, 66 + } 67 + } 68 + files[i] = file 69 + } 70 + result := types.RepoTreeResponse{ 71 + Ref: xrpcResp.Ref, 72 + Files: files, 73 + } 74 + if xrpcResp.Parent != nil { 75 + result.Parent = *xrpcResp.Parent 76 + } 77 + if xrpcResp.Dotdot != nil { 78 + result.DotDot = *xrpcResp.Dotdot 79 + } 80 + if xrpcResp.Readme != nil { 81 + result.ReadmeFileName = xrpcResp.Readme.Filename 82 + result.Readme = xrpcResp.Readme.Contents 83 + } 84 + // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated, 85 + // so we can safely redirect to the "parent" (which is the same file). 86 + if len(result.Files) == 0 && result.Parent == treePath { 87 + redirectTo := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), url.PathEscape(ref), result.Parent) 88 + http.Redirect(w, r, redirectTo, http.StatusFound) 89 + return 90 + } 91 + user := rp.oauth.GetUser(r) 92 + var breadcrumbs [][]string 93 + breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))}) 94 + if treePath != "" { 95 + for idx, elem := range strings.Split(treePath, "/") { 96 + breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))}) 97 + } 98 + } 99 + sortFiles(result.Files) 100 + rp.pages.RepoTree(w, pages.RepoTreeParams{ 101 + LoggedInUser: user, 102 + BreadCrumbs: breadcrumbs, 103 + TreePath: treePath, 104 + RepoInfo: f.RepoInfo(user), 105 + RepoTreeResponse: result, 106 + }) 107 + }

History

3 rounds 5 comments
sign up or login to add to the discussion
1 commit
expand
appview/repo: split up handlers into separate files
expand 0 comments
pull request successfully merged
1 commit
expand
appview/repo: split up handlers into separate files
expand 0 comments
oppi.li submitted #0
1 commit
expand
appview/repo: split up handlers into separate files
expand 5 comments

I would like to add handler_ prefix for all of those files. It will make file navigation way easier. Like https://tangled.org/@hailey.at/cocoon/tree/main/server

other than that, I'm 100% supportive to this idea.

I'd prefer we didn't. Go file name convention dictates using an underscore only when needed; I believe the the "handler" bit here is implicit.

perhaps that prefix would make sense if and when we introduce the service layer that we spoke about in earlier conversations.

I don't care much about pascelCase, but I wish we name "handler" explicitly. The repo module itself is already quite ambiguous by mixed with non-handler modules like appview/db.

So we can put the handler prefix to either repo module or each files. And for this case, as the commit message says we are splitting handlers into separate files, so putting prefix for individual files make sense.

I prefer prefix rather than suffix because it makes easier to spot non-prefixed files (like routers or util files) from file explorer.