Monorepo for Tangled
tangled.org
1package repo
2
3import (
4 "fmt"
5 "net/http"
6 "net/url"
7 "path"
8 "strings"
9 "time"
10
11 "tangled.org/core/api/tangled"
12 "tangled.org/core/appview/db"
13 "tangled.org/core/appview/pages"
14 "tangled.org/core/appview/pages/markup"
15 "tangled.org/core/appview/reporesolver"
16 xrpcclient "tangled.org/core/appview/xrpcclient"
17 "tangled.org/core/types"
18
19 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
20 "github.com/go-chi/chi/v5"
21 "github.com/go-git/go-git/v5/plumbing"
22)
23
24func (rp *Repo) Tree(w http.ResponseWriter, r *http.Request) {
25 l := rp.logger.With("handler", "RepoTree")
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 ref := chi.URLParam(r, "ref")
32 ref, _ = url.PathUnescape(ref)
33 // if the tree path has a trailing slash, let's strip it
34 // so we don't 404
35 treePath := chi.URLParam(r, "*")
36 treePath, _ = url.PathUnescape(treePath)
37 treePath = strings.TrimSuffix(treePath, "/")
38
39 xrpcc := &indigoxrpc.Client{Host: rp.config.KnotMirror.Url}
40 xrpcResp, err := tangled.GitTempGetTree(r.Context(), xrpcc, treePath, ref, f.RepoDid)
41 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
42 l.Error("failed to call XRPC repo.tree", "xrpcerr", xrpcerr, "err", err)
43 rp.pages.Error503(w)
44 return
45 }
46
47 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
48 // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
49 // so we can safely redirect to the "parent" (which is the same file).
50 if len(xrpcResp.Files) == 0 && xrpcResp.Parent != nil && *xrpcResp.Parent == treePath {
51 redirectTo := fmt.Sprintf("/%s/blob/%s/%s", ownerSlashRepo, url.PathEscape(ref), *xrpcResp.Parent)
52 http.Redirect(w, r, redirectTo, http.StatusFound)
53 return
54 }
55
56 var readmeFile *tangled.GitTempGetTree_TreeEntry
57 // Convert XRPC response to internal types.RepoTreeResponse
58 files := make([]types.NiceTree, len(xrpcResp.Files))
59 for i, xrpcFile := range xrpcResp.Files {
60 file := types.NiceTree{
61 Name: xrpcFile.Name,
62 Mode: xrpcFile.Mode,
63 Size: int64(xrpcFile.Size),
64 }
65 // Convert last commit info if present
66 if xrpcFile.Last_commit != nil {
67 commitWhen, _ := time.Parse(time.RFC3339, xrpcFile.Last_commit.When)
68 file.LastCommit = &types.LastCommitInfo{
69 Hash: plumbing.NewHash(xrpcFile.Last_commit.Hash),
70 Message: xrpcFile.Last_commit.Message,
71 When: commitWhen,
72 }
73 }
74 files[i] = file
75 if markup.IsReadmeFile(xrpcFile.Name, xrpcFile.Mode) {
76 readmeFile = xrpcFile
77 }
78 }
79 sortFiles(files)
80
81 var (
82 readmeFileName string
83 readmeFileContent string
84 )
85 if readmeFile != nil {
86 bytes, err := tangled.GitTempGetBlob(r.Context(), xrpcc, path.Join(treePath, readmeFile.Name), ref, f.RepoDid)
87 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
88 l.Error("failed to call XRPC git.getBlob", "xrpcerr", xrpcerr, "err", err)
89 rp.pages.Error503(w)
90 return
91 }
92 readmeFileName = readmeFile.Name
93 readmeFileContent = string(bytes)
94 }
95 var breadcrumbs [][]string
96 breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", ownerSlashRepo, url.PathEscape(ref))})
97 if treePath != "" {
98 for idx, elem := range strings.Split(treePath, "/") {
99 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
100 }
101 }
102
103 // Get email to DID mapping for commit author
104 var emails []string
105 if xrpcResp.LastCommit != nil && xrpcResp.LastCommit.Author != nil {
106 emails = append(emails, xrpcResp.LastCommit.Author.Email)
107 }
108 emailToDidMap, err := db.GetEmailToDid(rp.db, emails, true)
109 if err != nil {
110 l.Error("failed to get email to did mapping", "err", err)
111 emailToDidMap = make(map[string]string)
112 }
113
114 var lastCommitInfo *types.LastCommitInfo
115 if xrpcResp.LastCommit != nil {
116 when, _ := time.Parse(time.RFC3339, xrpcResp.LastCommit.When)
117 lastCommitInfo = &types.LastCommitInfo{
118 Hash: plumbing.NewHash(xrpcResp.LastCommit.Hash),
119 Message: xrpcResp.LastCommit.Message,
120 When: when,
121 }
122 if xrpcResp.LastCommit.Author != nil {
123 lastCommitInfo.Author.Name = xrpcResp.LastCommit.Author.Name
124 lastCommitInfo.Author.Email = xrpcResp.LastCommit.Author.Email
125 lastCommitInfo.Author.When, _ = time.Parse(time.RFC3339, xrpcResp.LastCommit.Author.When)
126 }
127 }
128
129 user := rp.oauth.GetMultiAccountUser(r)
130 rp.pages.RepoTree(w, pages.RepoTreeParams{
131 LoggedInUser: user,
132 BreadCrumbs: breadcrumbs,
133 Path: treePath,
134 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
135 EmailToDid: emailToDidMap,
136 LastCommitInfo: lastCommitInfo,
137 Ref: xrpcResp.Ref,
138 Parent: derefString(xrpcResp.Parent),
139 DotDot: derefString(xrpcResp.Dotdot),
140 Files: files,
141 ReadmeFileName: readmeFileName,
142 Readme: readmeFileContent,
143 })
144}
145
146func derefString(s *string) string {
147 if s == nil {
148 return ""
149 }
150 return *s
151}