A vibe coded tangled fork which supports pijul.
at master 306 lines 6.9 kB view raw
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}