A vibe coded tangled fork which supports pijul.
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}