A vibe coded tangled fork which supports pijul.
at master 202 lines 6.4 kB view raw
1package repo 2 3import ( 4 "fmt" 5 "net/http" 6 "net/url" 7 "strings" 8 "time" 9 10 "tangled.org/core/api/tangled" 11 "tangled.org/core/appview/db" 12 "tangled.org/core/appview/pages" 13 "tangled.org/core/appview/reporesolver" 14 xrpcclient "tangled.org/core/appview/xrpcclient" 15 "tangled.org/core/types" 16 17 indigoxrpc "github.com/bluesky-social/indigo/xrpc" 18 "github.com/go-chi/chi/v5" 19 "github.com/go-git/go-git/v5/plumbing" 20) 21 22func (rp *Repo) Tree(w http.ResponseWriter, r *http.Request) { 23 l := rp.logger.With("handler", "RepoTree") 24 f, err := rp.repoResolver.Resolve(r) 25 if err != nil { 26 l.Error("failed to fully resolve repo", "err", err) 27 return 28 } 29 ref := chi.URLParam(r, "ref") 30 ref, _ = url.PathUnescape(ref) 31 // if the tree path has a trailing slash, let's strip it 32 // so we don't 404 33 treePath := chi.URLParam(r, "*") 34 treePath, _ = url.PathUnescape(treePath) 35 treePath = strings.TrimSuffix(treePath, "/") 36 scheme := "http" 37 if !rp.config.Core.Dev { 38 scheme = "https" 39 } 40 host := fmt.Sprintf("%s://%s", scheme, f.Knot) 41 xrpcc := &indigoxrpc.Client{ 42 Host: host, 43 } 44 repo := fmt.Sprintf("%s/%s", f.Did, f.Name) 45 if f.IsPijul() { 46 xrpcResp, err := tangled.RepoPijulTree(r.Context(), xrpcc, ref, treePath, repo) 47 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 48 l.Error("failed to call XRPC repo.pijulTree", "err", xrpcerr) 49 rp.pages.Error503(w) 50 return 51 } 52 files := make([]types.NiceTree, len(xrpcResp.Files)) 53 for i, xrpcFile := range xrpcResp.Files { 54 files[i] = types.NiceTree{ 55 Name: xrpcFile.Name, 56 Mode: xrpcFile.Mode, 57 Size: xrpcFile.Size, 58 } 59 } 60 result := types.RepoTreeResponse{ 61 Ref: ref, 62 Files: files, 63 } 64 if xrpcResp.Ref != nil { 65 result.Ref = *xrpcResp.Ref 66 } 67 if xrpcResp.Parent != nil { 68 result.Parent = *xrpcResp.Parent 69 } 70 if xrpcResp.Dotdot != nil { 71 result.DotDot = *xrpcResp.Dotdot 72 } 73 if xrpcResp.Readme != nil { 74 if xrpcResp.Readme.Filename != nil { 75 result.ReadmeFileName = *xrpcResp.Readme.Filename 76 } 77 if xrpcResp.Readme.Contents != nil { 78 result.Readme = *xrpcResp.Readme.Contents 79 } 80 } 81 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 82 displayRef := ref 83 if displayRef == "" { 84 displayRef = result.Ref 85 } 86 if len(result.Files) == 0 && result.Parent == treePath { 87 redirectTo := fmt.Sprintf("/%s/blob/%s/%s", ownerSlashRepo, url.PathEscape(displayRef), result.Parent) 88 http.Redirect(w, r, redirectTo, http.StatusFound) 89 return 90 } 91 user := rp.oauth.GetMultiAccountUser(r) 92 var breadcrumbs [][]string 93 breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(displayRef))}) 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 Path: treePath, 104 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 105 RepoTreeResponse: result, 106 }) 107 return 108 } 109 110 xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo) 111 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil { 112 l.Error("failed to call XRPC repo.tree", "err", xrpcerr) 113 rp.pages.Error503(w) 114 return 115 } 116 // Convert XRPC response to internal types.RepoTreeResponse 117 files := make([]types.NiceTree, len(xrpcResp.Files)) 118 for i, xrpcFile := range xrpcResp.Files { 119 file := types.NiceTree{ 120 Name: xrpcFile.Name, 121 Mode: xrpcFile.Mode, 122 Size: int64(xrpcFile.Size), 123 } 124 // Convert last commit info if present 125 if xrpcFile.Last_commit != nil { 126 commitWhen, _ := time.Parse(time.RFC3339, xrpcFile.Last_commit.When) 127 file.LastCommit = &types.LastCommitInfo{ 128 Hash: plumbing.NewHash(xrpcFile.Last_commit.Hash), 129 Message: xrpcFile.Last_commit.Message, 130 When: commitWhen, 131 } 132 } 133 files[i] = file 134 } 135 result := types.RepoTreeResponse{ 136 Ref: xrpcResp.Ref, 137 Files: files, 138 } 139 if xrpcResp.Parent != nil { 140 result.Parent = *xrpcResp.Parent 141 } 142 if xrpcResp.Dotdot != nil { 143 result.DotDot = *xrpcResp.Dotdot 144 } 145 if xrpcResp.Readme != nil { 146 result.ReadmeFileName = xrpcResp.Readme.Filename 147 result.Readme = xrpcResp.Readme.Contents 148 } 149 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f) 150 // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated, 151 // so we can safely redirect to the "parent" (which is the same file). 152 if len(result.Files) == 0 && result.Parent == treePath { 153 redirectTo := fmt.Sprintf("/%s/blob/%s/%s", ownerSlashRepo, url.PathEscape(ref), result.Parent) 154 http.Redirect(w, r, redirectTo, http.StatusFound) 155 return 156 } 157 user := rp.oauth.GetMultiAccountUser(r) 158 var breadcrumbs [][]string 159 breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))}) 160 if treePath != "" { 161 for idx, elem := range strings.Split(treePath, "/") { 162 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))}) 163 } 164 } 165 sortFiles(result.Files) 166 167 // Get email to DID mapping for commit author 168 var emails []string 169 if xrpcResp.LastCommit != nil && xrpcResp.LastCommit.Author != nil { 170 emails = append(emails, xrpcResp.LastCommit.Author.Email) 171 } 172 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true) 173 if err != nil { 174 l.Error("failed to get email to did mapping", "err", err) 175 emailToDidMap = make(map[string]string) 176 } 177 178 var lastCommitInfo *types.LastCommitInfo 179 if xrpcResp.LastCommit != nil { 180 when, _ := time.Parse(time.RFC3339, xrpcResp.LastCommit.When) 181 lastCommitInfo = &types.LastCommitInfo{ 182 Hash: plumbing.NewHash(xrpcResp.LastCommit.Hash), 183 Message: xrpcResp.LastCommit.Message, 184 When: when, 185 } 186 if xrpcResp.LastCommit.Author != nil { 187 lastCommitInfo.Author.Name = xrpcResp.LastCommit.Author.Name 188 lastCommitInfo.Author.Email = xrpcResp.LastCommit.Author.Email 189 lastCommitInfo.Author.When, _ = time.Parse(time.RFC3339, xrpcResp.LastCommit.Author.When) 190 } 191 } 192 193 rp.pages.RepoTree(w, pages.RepoTreeParams{ 194 LoggedInUser: user, 195 BreadCrumbs: breadcrumbs, 196 Path: treePath, 197 RepoInfo: rp.repoResolver.GetRepoInfo(r, user), 198 EmailToDid: emailToDidMap, 199 LastCommitInfo: lastCommitInfo, 200 RepoTreeResponse: result, 201 }) 202}