A vibe coded tangled fork which supports pijul.
1package xrpc
2
3import (
4 "net/http"
5 "strconv"
6 "time"
7
8 "tangled.org/core/knotserver/vcs"
9 xrpcerr "tangled.org/core/xrpc/errors"
10)
11
12// PijulChangeListResponse is the response for listing Pijul changes
13type PijulChangeListResponse struct {
14 Changes []PijulChangeEntry `json:"changes"`
15 Channel string `json:"channel,omitempty"`
16 Page int `json:"page"`
17 PerPage int `json:"per_page"`
18 Total int `json:"total"`
19}
20
21// PijulChangeEntry represents a single change in the list
22type PijulChangeEntry struct {
23 Hash string `json:"hash"`
24 Authors []PijulAuthor `json:"authors"`
25 Message string `json:"message"`
26 Timestamp string `json:"timestamp,omitempty"`
27 Dependencies []string `json:"dependencies,omitempty"`
28}
29
30// PijulAuthor represents a change author
31type PijulAuthor struct {
32 Name string `json:"name"`
33 Email string `json:"email,omitempty"`
34}
35
36// RepoChangeList handles the sh.tangled.repo.changeList endpoint
37// Lists changes (Pijul equivalent of commits) in a repository.
38// Uses the unified VCS History interface.
39func (x *Xrpc) RepoChangeList(w http.ResponseWriter, r *http.Request) {
40 repo := r.URL.Query().Get("repo")
41 repoPath, err := x.parseRepoParam(repo)
42 if err != nil {
43 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
44 return
45 }
46
47 channel := r.URL.Query().Get("channel")
48 cursor := r.URL.Query().Get("cursor")
49
50 limit := 50 // default
51 if limitStr := r.URL.Query().Get("limit"); limitStr != "" {
52 if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
53 limit = l
54 }
55 }
56
57 rv, err := vcs.Open(repoPath, channel)
58 if err != nil {
59 writeError(w, xrpcerr.NewXrpcError(
60 xrpcerr.WithTag("RepoNotFound"),
61 xrpcerr.WithMessage("failed to open repository"),
62 ), http.StatusNotFound)
63 return
64 }
65
66 offset := 0
67 if cursor != "" {
68 if o, err := strconv.Atoi(cursor); err == nil && o >= 0 {
69 offset = o
70 }
71 }
72
73 entries, err := rv.History(offset, limit)
74 if err != nil {
75 x.Logger.Error("fetching changes", "error", err.Error())
76 writeError(w, xrpcerr.NewXrpcError(
77 xrpcerr.WithTag("InternalServerError"),
78 xrpcerr.WithMessage("failed to read change log"),
79 ), http.StatusInternalServerError)
80 return
81 }
82
83 total, err := rv.TotalHistoryEntries()
84 if err != nil {
85 x.Logger.Error("fetching total changes", "error", err.Error())
86 writeError(w, xrpcerr.NewXrpcError(
87 xrpcerr.WithTag("InternalServerError"),
88 xrpcerr.WithMessage("failed to fetch total changes"),
89 ), http.StatusInternalServerError)
90 return
91 }
92
93 // Convert to response format
94 changeEntries := make([]PijulChangeEntry, len(entries))
95 for i, e := range entries {
96 authors := []PijulAuthor{{
97 Name: e.Author.Name,
98 Email: e.Author.Email,
99 }}
100
101 changeEntries[i] = PijulChangeEntry{
102 Hash: e.Hash,
103 Authors: authors,
104 Message: e.Message,
105 Dependencies: e.Parents,
106 }
107
108 if !e.Timestamp.IsZero() {
109 changeEntries[i].Timestamp = e.Timestamp.Format(time.RFC3339)
110 }
111 }
112
113 response := PijulChangeListResponse{
114 Changes: changeEntries,
115 Channel: channel,
116 Page: (offset / limit) + 1,
117 PerPage: limit,
118 Total: total,
119 }
120
121 writeJson(w, response)
122}
123
124// PijulChangeGetResponse is the response for getting a single change
125type PijulChangeGetResponse struct {
126 Hash string `json:"hash"`
127 Authors []PijulAuthor `json:"authors"`
128 Message string `json:"message"`
129 Timestamp string `json:"timestamp,omitempty"`
130 Dependencies []string `json:"dependencies,omitempty"`
131 Diff string `json:"diff,omitempty"`
132}
133
134// RepoChangeGet handles the sh.tangled.repo.changeGet endpoint
135// Gets details for a specific change
136func (x *Xrpc) RepoChangeGet(w http.ResponseWriter, r *http.Request) {
137 repo := r.URL.Query().Get("repo")
138 repoPath, err := x.parseRepoParam(repo)
139 if err != nil {
140 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest)
141 return
142 }
143
144 hash := r.URL.Query().Get("hash")
145 if hash == "" {
146 writeError(w, xrpcerr.NewXrpcError(
147 xrpcerr.WithTag("InvalidRequest"),
148 xrpcerr.WithMessage("missing hash parameter"),
149 ), http.StatusBadRequest)
150 return
151 }
152
153 rv, err := vcs.Open(repoPath, "")
154 if err != nil {
155 writeError(w, xrpcerr.NewXrpcError(
156 xrpcerr.WithTag("RepoNotFound"),
157 xrpcerr.WithMessage("failed to open repository"),
158 ), http.StatusNotFound)
159 return
160 }
161
162 entry, err := rv.HistoryEntry(hash)
163 if err != nil {
164 x.Logger.Error("fetching change", "error", err.Error(), "hash", hash)
165 writeError(w, xrpcerr.NewXrpcError(
166 xrpcerr.WithTag("ChangeNotFound"),
167 xrpcerr.WithMessage("change not found"),
168 ), http.StatusNotFound)
169 return
170 }
171
172 authors := []PijulAuthor{{
173 Name: entry.Author.Name,
174 Email: entry.Author.Email,
175 }}
176
177 response := PijulChangeGetResponse{
178 Hash: entry.Hash,
179 Authors: authors,
180 Message: entry.Message,
181 Dependencies: entry.Parents,
182 }
183
184 if !entry.Timestamp.IsZero() {
185 response.Timestamp = entry.Timestamp.Format(time.RFC3339)
186 }
187
188 // Get diff for pijul repos
189 if pr := vcs.AsPijul(rv); pr != nil {
190 diff, err := pr.DiffChange(hash)
191 if err != nil {
192 x.Logger.Warn("failed to get diff for change", "hash", hash, "error", err)
193 } else if diff != nil {
194 response.Diff = diff.Raw
195 }
196 }
197
198 writeJson(w, response)
199}