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/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
20func (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}