A vibe coded tangled fork which supports pijul.
1package xrpc
2
3import (
4 "net/http"
5 "path/filepath"
6 "time"
7 "unicode/utf8"
8
9 "github.com/go-git/go-git/v5/plumbing"
10 "tangled.org/core/api/tangled"
11 "tangled.org/core/appview/pages/markup"
12 "tangled.org/core/knotserver/vcs"
13 xrpcerr "tangled.org/core/xrpc/errors"
14)
15
16func (x *Xrpc) RepoTree(w http.ResponseWriter, r *http.Request) {
17 ctx := r.Context()
18
19 repo := r.URL.Query().Get("repo")
20 repoPath, err := x.parseRepoParam(repo)
21 if err != nil {
22 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
23 return
24 }
25
26 ref := r.URL.Query().Get("ref")
27 // For pijul, also accept "channel" param and fall back to default
28 if channel := r.URL.Query().Get("channel"); channel != "" {
29 ref = channel
30 }
31
32 path := r.URL.Query().Get("path")
33
34 rv, err := vcs.Open(repoPath, ref)
35 if err != nil {
36 x.Logger.Error("failed to open repository", "error", err, "path", repoPath, "ref", ref)
37 writeError(w, xrpcerr.RefNotFoundError, http.StatusNotFound)
38 return
39 }
40
41 files, err := rv.FileTree(ctx, path)
42 if err != nil {
43 x.Logger.Error("failed to get file tree", "error", err, "path", path)
44 writeError(w, xrpcerr.NewXrpcError(
45 xrpcerr.WithTag("PathNotFound"),
46 xrpcerr.WithMessage("failed to read repository tree"),
47 ), http.StatusNotFound)
48 return
49 }
50
51 // if any of these files are a readme candidate, pass along its blob contents too
52 var readmeFileName string
53 var readmeContents string
54 for _, file := range files {
55 if markup.IsReadmeFile(file.Name) {
56 contents, err := rv.RawContent(filepath.Join(path, file.Name))
57 if err != nil {
58 x.Logger.Error("failed to read contents of file", "path", path, "file", file.Name)
59 continue
60 }
61
62 if utf8.Valid(contents) {
63 readmeFileName = file.Name
64 readmeContents = string(contents)
65 break
66 }
67 }
68 }
69
70 // convert vcs.TreeEntry -> tangled.RepoTree_TreeEntry
71 treeEntries := make([]*tangled.RepoTree_TreeEntry, len(files))
72 for i, file := range files {
73 entry := &tangled.RepoTree_TreeEntry{
74 Name: file.Name,
75 Mode: file.Mode,
76 Size: file.Size,
77 }
78
79 if file.LastCommit != nil {
80 entry.Last_commit = &tangled.RepoTree_LastCommit{
81 Hash: file.LastCommit.Hash,
82 Message: file.LastCommit.Message,
83 When: file.LastCommit.When.Format(time.RFC3339),
84 }
85 }
86
87 treeEntries[i] = entry
88 }
89
90 var parentPtr *string
91 if path != "" {
92 parentPtr = &path
93 }
94
95 var dotdotPtr *string
96 if path != "" {
97 dotdot := filepath.Dir(path)
98 if dotdot != "." {
99 dotdotPtr = &dotdot
100 }
101 }
102
103 response := tangled.RepoTree_Output{
104 Ref: ref,
105 Parent: parentPtr,
106 Dotdot: dotdotPtr,
107 Files: treeEntries,
108 Readme: &tangled.RepoTree_Readme{
109 Filename: readmeFileName,
110 Contents: readmeContents,
111 },
112 }
113
114 // calculate lastCommit for the directory as a whole
115 var lastCommitTree *vcs.LastCommitInfo
116 for i := range files {
117 e := &files[i]
118 if e.LastCommit == nil {
119 continue
120 }
121
122 if lastCommitTree == nil {
123 lastCommitTree = e.LastCommit
124 continue
125 }
126
127 if lastCommitTree.When.Before(e.LastCommit.When) {
128 lastCommitTree = e.LastCommit
129 }
130 }
131
132 if lastCommitTree != nil {
133 response.LastCommit = &tangled.RepoTree_LastCommit{
134 Hash: lastCommitTree.Hash,
135 Message: lastCommitTree.Message,
136 When: lastCommitTree.When.Format(time.RFC3339),
137 }
138
139 if lastCommitTree.Author != nil {
140 response.LastCommit.Author = &tangled.RepoTree_Signature{
141 Name: lastCommitTree.Author.Name,
142 Email: lastCommitTree.Author.Email,
143 }
144 } else if gr := vcs.AsGit(rv); gr != nil {
145 // try to get author information from the git commit
146 commit, err := gr.Commit(plumbing.NewHash(lastCommitTree.Hash))
147 if err == nil {
148 response.LastCommit.Author = &tangled.RepoTree_Signature{
149 Name: commit.Author.Name,
150 Email: commit.Author.Email,
151 }
152 }
153 }
154 }
155
156 writeJson(w, response)
157}