A vibe coded tangled fork which supports pijul.
1package repo
2
3import (
4 "fmt"
5 "net/http"
6 "net/url"
7 "strconv"
8 "strings"
9 "time"
10
11 "tangled.org/core/api/tangled"
12 "tangled.org/core/appview/pages"
13 xrpcclient "tangled.org/core/appview/xrpcclient"
14
15 indigoxrpc "github.com/bluesky-social/indigo/xrpc"
16 "github.com/go-chi/chi/v5"
17)
18
19func (rp *Repo) Changes(w http.ResponseWriter, r *http.Request) {
20 l := rp.logger.With("handler", "RepoChanges")
21
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 if !f.IsPijul() {
28 rp.pages.Error404(w)
29 return
30 }
31
32 page := 1
33 if r.URL.Query().Get("page") != "" {
34 page, err = strconv.Atoi(r.URL.Query().Get("page"))
35 if err != nil {
36 page = 1
37 }
38 }
39
40 ref := chi.URLParam(r, "ref")
41 ref, _ = url.PathUnescape(ref)
42
43 scheme := "http"
44 if !rp.config.Core.Dev {
45 scheme = "https"
46 }
47 host := fmt.Sprintf("%s://%s", scheme, f.Knot)
48 xrpcc := &indigoxrpc.Client{
49 Host: host,
50 }
51
52 repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
53
54 if ref == "" {
55 channels, err := tangled.RepoChannelList(r.Context(), xrpcc, "", 0, repo)
56 if err != nil {
57 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
58 l.Error("failed to call XRPC repo.channelList", "err", xrpcerr)
59 rp.pages.Error503(w)
60 return
61 }
62 rp.pages.Error503(w)
63 return
64 }
65 for _, ch := range channels.Channels {
66 if ch.Is_current != nil && *ch.Is_current {
67 ref = ch.Name
68 break
69 }
70 }
71 if ref == "" && len(channels.Channels) > 0 {
72 ref = channels.Channels[0].Name
73 }
74 }
75
76 limit := int64(60)
77 cursor := ""
78 if page > 1 {
79 offset := (page - 1) * int(limit)
80 cursor = strconv.Itoa(offset)
81 }
82
83 resp, err := tangled.RepoChangeList(r.Context(), xrpcc, ref, cursor, limit, repo)
84 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
85 l.Error("failed to call XRPC repo.changeList", "err", xrpcerr)
86 rp.pages.Error503(w)
87 return
88 }
89
90 changes := make([]pages.PijulChangeView, 0, len(resp.Changes))
91 for _, change := range resp.Changes {
92 view := pages.PijulChangeView{
93 Hash: change.Hash,
94 Authors: change.Authors,
95 Message: change.Message,
96 Dependencies: change.Dependencies,
97 }
98 if change.Timestamp != nil {
99 if parsed, err := time.Parse(time.RFC3339, *change.Timestamp); err == nil {
100 view.Timestamp = parsed
101 view.HasTimestamp = true
102 }
103 }
104 changes = append(changes, view)
105 }
106
107 user := rp.oauth.GetMultiAccountUser(r)
108 rp.pages.RepoChanges(w, pages.RepoChangesParams{
109 LoggedInUser: user,
110 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
111 Page: page,
112 Changes: changes,
113 })
114}
115
116func (rp *Repo) Change(w http.ResponseWriter, r *http.Request) {
117 l := rp.logger.With("handler", "RepoChange")
118
119 f, err := rp.repoResolver.Resolve(r)
120 if err != nil {
121 l.Error("failed to fully resolve repo", "err", err)
122 return
123 }
124 if !f.IsPijul() {
125 rp.pages.Error404(w)
126 return
127 }
128
129 hash := chi.URLParam(r, "hash")
130 if hash == "" {
131 rp.pages.Error404(w)
132 return
133 }
134
135 scheme := "http"
136 if !rp.config.Core.Dev {
137 scheme = "https"
138 }
139 host := fmt.Sprintf("%s://%s", scheme, f.Knot)
140 xrpcc := &indigoxrpc.Client{
141 Host: host,
142 }
143
144 repo := fmt.Sprintf("%s/%s", f.Did, f.Name)
145 resp, err := tangled.RepoChangeGet(r.Context(), xrpcc, hash, repo)
146 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
147 l.Error("failed to call XRPC repo.changeGet", "err", xrpcerr)
148 rp.pages.Error503(w)
149 return
150 }
151
152 change := pages.PijulChangeDetail{
153 Hash: resp.Hash,
154 Authors: resp.Authors,
155 Message: resp.Message,
156 Dependencies: resp.Dependencies,
157 }
158 if resp.Diff != nil {
159 change.Diff = *resp.Diff
160 change.HasDiff = true
161 change.DiffLines = parsePijulDiffLines(change.Diff)
162 }
163 if resp.Timestamp != nil {
164 if parsed, err := time.Parse(time.RFC3339, *resp.Timestamp); err == nil {
165 change.Timestamp = parsed
166 change.HasTimestamp = true
167 }
168 }
169
170 user := rp.oauth.GetMultiAccountUser(r)
171 rp.pages.RepoChange(w, pages.RepoChangeParams{
172 LoggedInUser: user,
173 RepoInfo: rp.repoResolver.GetRepoInfo(r, user),
174 Change: change,
175 })
176}
177
178func parsePijulDiffLines(diff string) []pages.PijulDiffLine {
179 if diff == "" {
180 return nil
181 }
182 lines := strings.Split(diff, "\n")
183 out := make([]pages.PijulDiffLine, 0, len(lines))
184 var oldLine int64
185 var newLine int64
186 var hasOld bool
187 var hasNew bool
188 for _, line := range lines {
189 kind := "context"
190 op := " "
191 body := line
192 switch {
193 case strings.HasPrefix(line, "+++ ") || strings.HasPrefix(line, "--- "):
194 kind = "meta"
195 op = ""
196 hasOld = false
197 hasNew = false
198 case strings.HasPrefix(line, "@@"):
199 kind = "meta"
200 op = ""
201 if o, n, ok := parseUnifiedHunkHeader(line); ok {
202 oldLine = o
203 newLine = n
204 hasOld = true
205 hasNew = true
206 } else {
207 hasOld = false
208 hasNew = false
209 }
210 case strings.HasPrefix(line, "diff ") || strings.HasPrefix(line, "index "):
211 kind = "meta"
212 op = ""
213 hasOld = false
214 hasNew = false
215 case strings.HasPrefix(line, "#"):
216 kind = "section"
217 op = ""
218 hasOld = false
219 hasNew = false
220 case strings.HasPrefix(line, "+") && !strings.HasPrefix(line, "+++"):
221 kind = "add"
222 op = "+"
223 body = line[1:]
224 case strings.HasPrefix(line, "-") && !strings.HasPrefix(line, "---"):
225 kind = "del"
226 op = "-"
227 body = line[1:]
228 case strings.HasPrefix(line, " "):
229 body = line[1:]
230 }
231 diffLine := pages.PijulDiffLine{
232 Kind: kind,
233 Op: op,
234 Body: body,
235 Text: line,
236 }
237 if kind != "meta" {
238 if kind == "del" {
239 if hasOld {
240 diffLine.OldLine = oldLine
241 diffLine.HasOld = true
242 oldLine++
243 }
244 } else if kind == "add" {
245 if hasNew {
246 diffLine.NewLine = newLine
247 diffLine.HasNew = true
248 newLine++
249 }
250 } else {
251 if hasOld {
252 diffLine.OldLine = oldLine
253 diffLine.HasOld = true
254 oldLine++
255 }
256 if hasNew {
257 diffLine.NewLine = newLine
258 diffLine.HasNew = true
259 newLine++
260 }
261 }
262 }
263 out = append(out, diffLine)
264 }
265 return out
266}
267
268func parseUnifiedHunkHeader(line string) (int64, int64, bool) {
269 start := strings.Index(line, "@@")
270 if start == -1 {
271 return 0, 0, false
272 }
273 trimmed := strings.TrimSpace(line[start+2:])
274 end := strings.Index(trimmed, "@@")
275 if end == -1 {
276 return 0, 0, false
277 }
278 fields := strings.Fields(strings.TrimSpace(trimmed[:end]))
279 if len(fields) < 2 {
280 return 0, 0, false
281 }
282 oldStart, okOld := parseUnifiedRange(fields[0], "-")
283 newStart, okNew := parseUnifiedRange(fields[1], "+")
284 if !okOld || !okNew {
285 return 0, 0, false
286 }
287 return oldStart, newStart, true
288}
289
290func parseUnifiedRange(value, prefix string) (int64, bool) {
291 if !strings.HasPrefix(value, prefix) {
292 return 0, false
293 }
294 value = strings.TrimPrefix(value, prefix)
295 if value == "" {
296 return 0, false
297 }
298 if idx := strings.Index(value, ","); idx >= 0 {
299 value = value[:idx]
300 }
301 out, err := strconv.ParseInt(value, 10, 64)
302 if err != nil {
303 return 0, false
304 }
305 return out, true
306}