package xrpc import ( "net/http" "strconv" "time" "tangled.org/core/knotserver/vcs" xrpcerr "tangled.org/core/xrpc/errors" ) // PijulChangeListResponse is the response for listing Pijul changes type PijulChangeListResponse struct { Changes []PijulChangeEntry `json:"changes"` Channel string `json:"channel,omitempty"` Page int `json:"page"` PerPage int `json:"per_page"` Total int `json:"total"` } // PijulChangeEntry represents a single change in the list type PijulChangeEntry struct { Hash string `json:"hash"` Authors []PijulAuthor `json:"authors"` Message string `json:"message"` Timestamp string `json:"timestamp,omitempty"` Dependencies []string `json:"dependencies,omitempty"` } // PijulAuthor represents a change author type PijulAuthor struct { Name string `json:"name"` Email string `json:"email,omitempty"` } // RepoChangeList handles the sh.tangled.repo.changeList endpoint // Lists changes (Pijul equivalent of commits) in a repository. // Uses the unified VCS History interface. func (x *Xrpc) RepoChangeList(w http.ResponseWriter, r *http.Request) { repo := r.URL.Query().Get("repo") repoPath, err := x.parseRepoParam(repo) if err != nil { writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) return } channel := r.URL.Query().Get("channel") cursor := r.URL.Query().Get("cursor") limit := 50 // default if limitStr := r.URL.Query().Get("limit"); limitStr != "" { if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 { limit = l } } rv, err := vcs.Open(repoPath, channel) if err != nil { writeError(w, xrpcerr.NewXrpcError( xrpcerr.WithTag("RepoNotFound"), xrpcerr.WithMessage("failed to open repository"), ), http.StatusNotFound) return } offset := 0 if cursor != "" { if o, err := strconv.Atoi(cursor); err == nil && o >= 0 { offset = o } } entries, err := rv.History(offset, limit) if err != nil { x.Logger.Error("fetching changes", "error", err.Error()) writeError(w, xrpcerr.NewXrpcError( xrpcerr.WithTag("InternalServerError"), xrpcerr.WithMessage("failed to read change log"), ), http.StatusInternalServerError) return } total, err := rv.TotalHistoryEntries() if err != nil { x.Logger.Error("fetching total changes", "error", err.Error()) writeError(w, xrpcerr.NewXrpcError( xrpcerr.WithTag("InternalServerError"), xrpcerr.WithMessage("failed to fetch total changes"), ), http.StatusInternalServerError) return } // Convert to response format changeEntries := make([]PijulChangeEntry, len(entries)) for i, e := range entries { authors := []PijulAuthor{{ Name: e.Author.Name, Email: e.Author.Email, }} changeEntries[i] = PijulChangeEntry{ Hash: e.Hash, Authors: authors, Message: e.Message, Dependencies: e.Parents, } if !e.Timestamp.IsZero() { changeEntries[i].Timestamp = e.Timestamp.Format(time.RFC3339) } } response := PijulChangeListResponse{ Changes: changeEntries, Channel: channel, Page: (offset / limit) + 1, PerPage: limit, Total: total, } writeJson(w, response) } // PijulChangeGetResponse is the response for getting a single change type PijulChangeGetResponse struct { Hash string `json:"hash"` Authors []PijulAuthor `json:"authors"` Message string `json:"message"` Timestamp string `json:"timestamp,omitempty"` Dependencies []string `json:"dependencies,omitempty"` Diff string `json:"diff,omitempty"` } // RepoChangeGet handles the sh.tangled.repo.changeGet endpoint // Gets details for a specific change func (x *Xrpc) RepoChangeGet(w http.ResponseWriter, r *http.Request) { repo := r.URL.Query().Get("repo") repoPath, err := x.parseRepoParam(repo) if err != nil { writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) return } hash := r.URL.Query().Get("hash") if hash == "" { writeError(w, xrpcerr.NewXrpcError( xrpcerr.WithTag("InvalidRequest"), xrpcerr.WithMessage("missing hash parameter"), ), http.StatusBadRequest) return } rv, err := vcs.Open(repoPath, "") if err != nil { writeError(w, xrpcerr.NewXrpcError( xrpcerr.WithTag("RepoNotFound"), xrpcerr.WithMessage("failed to open repository"), ), http.StatusNotFound) return } entry, err := rv.HistoryEntry(hash) if err != nil { x.Logger.Error("fetching change", "error", err.Error(), "hash", hash) writeError(w, xrpcerr.NewXrpcError( xrpcerr.WithTag("ChangeNotFound"), xrpcerr.WithMessage("change not found"), ), http.StatusNotFound) return } authors := []PijulAuthor{{ Name: entry.Author.Name, Email: entry.Author.Email, }} response := PijulChangeGetResponse{ Hash: entry.Hash, Authors: authors, Message: entry.Message, Dependencies: entry.Parents, } if !entry.Timestamp.IsZero() { response.Timestamp = entry.Timestamp.Format(time.RFC3339) } // Get diff for pijul repos if pr := vcs.AsPijul(rv); pr != nil { diff, err := pr.DiffChange(hash) if err != nil { x.Logger.Warn("failed to get diff for change", "hash", hash, "error", err) } else if diff != nil { response.Diff = diff.Raw } } writeJson(w, response) }