forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
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 "tangled.org/core/appview/reporesolver"
13 xrpcclient "tangled.org/core/appview/xrpcclient"
14 "tangled.org/core/types"
15
16 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
17 "github.com/go-chi/chi/v5"
18 "github.com/go-git/go-git/v5/plumbing"
19)
20
21func (rp *Repo) Tree(w http.ResponseWriter, r *http.Request) {
22 l := rp.logger.With("handler", "RepoTree")
23 f, err := rp.repoResolver.Resolve(r)
24 if err != nil {
25 l.Error("failed to fully resolve repo", "err", err)
26 return
27 }
28 ref := chi.URLParam(r, "ref")
29 ref, _ = url.PathUnescape(ref)
30 // if the tree path has a trailing slash, let's strip it
31 // so we don't 404
32 treePath := chi.URLParam(r, "*")
33 treePath, _ = url.PathUnescape(treePath)
34 treePath = strings.TrimSuffix(treePath, "/")
35 scheme := "http"
36 if !rp.config.Core.Dev {
37 scheme = "https"
38 }
39 host := fmt.Sprintf("%s://%s", scheme, f.Knot)
40 xrpcc := &indigoxrpc.Client{
41 Host: host,
42 }
43 repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
44 xrpcResp, err := tangled.RepoTree(r.Context(), xrpcc, treePath, ref, repo)
45 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
46 l.Error("failed to call XRPC repo.tree", "err", xrpcerr)
47 rp.pages.Error503(w)
48 return
49 }
50 // Convert XRPC response to internal types.RepoTreeResponse
51 files := make([]types.NiceTree, len(xrpcResp.Files))
52 for i, xrpcFile := range xrpcResp.Files {
53 file := types.NiceTree{
54 Name: xrpcFile.Name,
55 Mode: xrpcFile.Mode,
56 Size: int64(xrpcFile.Size),
57 }
58 // Convert last commit info if present
59 if xrpcFile.Last_commit != nil {
60 commitWhen, _ := time.Parse(time.RFC3339, xrpcFile.Last_commit.When)
61 file.LastCommit = &types.LastCommitInfo{
62 Hash: plumbing.NewHash(xrpcFile.Last_commit.Hash),
63 Message: xrpcFile.Last_commit.Message,
64 When: commitWhen,
65 }
66 }
67 files[i] = file
68 }
69 result := types.RepoTreeResponse{
70 Ref: xrpcResp.Ref,
71 Files: files,
72 }
73 if xrpcResp.Parent != nil {
74 result.Parent = *xrpcResp.Parent
75 }
76 if xrpcResp.Dotdot != nil {
77 result.DotDot = *xrpcResp.Dotdot
78 }
79 if xrpcResp.Readme != nil {
80 result.ReadmeFileName = xrpcResp.Readme.Filename
81 result.Readme = xrpcResp.Readme.Contents
82 }
83 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
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", 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", 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
101 rp.pages.RepoTree(w, pages.RepoTreeParams{
102 LoggedInUser: user,
103 BreadCrumbs: breadcrumbs,
104 TreePath: treePath,
105 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
106 RepoTreeResponse: result,
107 })
108}