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 }
57 // Convert last commit info if present
58 if xrpcFile.Last_commit != nil {
59 commitWhen, _ := time.Parse(time.RFC3339, xrpcFile.Last_commit.When)
60 file.LastCommit = &types.LastCommitInfo{
61 Hash: plumbing.NewHash(xrpcFile.Last_commit.Hash),
62 Message: xrpcFile.Last_commit.Message,
63 When: commitWhen,
64 }
65 }
66 files[i] = file
67 }
68 result := types.RepoTreeResponse{
69 Ref: xrpcResp.Ref,
70 Files: files,
71 }
72 if xrpcResp.Parent != nil {
73 result.Parent = *xrpcResp.Parent
74 }
75 if xrpcResp.Dotdot != nil {
76 result.DotDot = *xrpcResp.Dotdot
77 }
78 if xrpcResp.Readme != nil {
79 result.ReadmeFileName = xrpcResp.Readme.Filename
80 result.Readme = xrpcResp.Readme.Contents
81 }
82 // redirects tree paths trying to access a blob; in this case the result.Files is unpopulated,
83 // so we can safely redirect to the "parent" (which is the same file).
84 if len(result.Files) == 0 && result.Parent == treePath {
85 redirectTo := fmt.Sprintf("/%s/blob/%s/%s", f.OwnerSlashRepo(), url.PathEscape(ref), result.Parent)
86 http.Redirect(w, r, redirectTo, http.StatusFound)
87 return
88 }
89 user := rp.oauth.GetUser(r)
90 var breadcrumbs [][]string
91 breadcrumbs = append(breadcrumbs, []string{f.Name, fmt.Sprintf("/%s/tree/%s", f.OwnerSlashRepo(), url.PathEscape(ref))})
92 if treePath != "" {
93 for idx, elem := range strings.Split(treePath, "/") {
94 breadcrumbs = append(breadcrumbs, []string{elem, fmt.Sprintf("%s/%s", breadcrumbs[idx][1], url.PathEscape(elem))})
95 }
96 }
97 sortFiles(result.Files)
98
99 rp.pages.RepoTree(w, pages.RepoTreeParams{
100 LoggedInUser: user,
101 BreadCrumbs: breadcrumbs,
102 TreePath: treePath,
103 RepoInfo: f.RepoInfo(user),
104 RepoTreeResponse: result,
105 })
106}