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