A vibe coded tangled fork which supports pijul.
at master 158 lines 4.5 kB view raw
1package xrpc 2 3import ( 4 "encoding/json" 5 "net/http" 6 "strings" 7 8 "github.com/bluesky-social/indigo/atproto/syntax" 9 securejoin "github.com/cyphar/filepath-securejoin" 10 "tangled.org/core/knotserver/pijul" 11 "tangled.org/core/rbac" 12 xrpcerr "tangled.org/core/xrpc/errors" 13) 14 15// ApplyChangesRequest is the request body for applying changes 16type ApplyChangesRequest struct { 17 Repo string `json:"repo"` 18 Channel string `json:"channel"` 19 Changes []string `json:"changes"` 20} 21 22// ApplyChangesResponse is the response for applying changes 23type ApplyChangesResponse struct { 24 Applied []string `json:"applied"` 25 Failed []ApplyChangeFailure `json:"failed,omitempty"` 26} 27 28// ApplyChangeFailure represents a failed change application 29type ApplyChangeFailure struct { 30 Hash string `json:"hash"` 31 Error string `json:"error"` 32} 33 34// RepoApplyChanges handles the sh.tangled.repo.applyChanges endpoint 35// Applies Pijul changes to a repository channel (used for merging discussions) 36func (x *Xrpc) RepoApplyChanges(w http.ResponseWriter, r *http.Request) { 37 if r.Method != http.MethodPost { 38 writeError(w, xrpcerr.NewXrpcError( 39 xrpcerr.WithTag("InvalidRequest"), 40 xrpcerr.WithMessage("method not allowed"), 41 ), http.StatusMethodNotAllowed) 42 return 43 } 44 45 var req ApplyChangesRequest 46 if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 47 writeError(w, xrpcerr.NewXrpcError( 48 xrpcerr.WithTag("InvalidRequest"), 49 xrpcerr.WithMessage("invalid request body"), 50 ), http.StatusBadRequest) 51 return 52 } 53 54 if req.Repo == "" || req.Channel == "" || len(req.Changes) == 0 { 55 writeError(w, xrpcerr.NewXrpcError( 56 xrpcerr.WithTag("InvalidRequest"), 57 xrpcerr.WithMessage("repo, channel, and changes are required"), 58 ), http.StatusBadRequest) 59 return 60 } 61 62 // Authorization: verify the caller has push permission 63 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID) 64 if !ok { 65 writeError(w, xrpcerr.MissingActorDidError, http.StatusBadRequest) 66 return 67 } 68 69 repoPath, err := x.parseRepoParam(req.Repo) 70 if err != nil { 71 writeError(w, err.(xrpcerr.XrpcError), http.StatusBadRequest) 72 return 73 } 74 75 repoParts := strings.SplitN(req.Repo, "/", 2) 76 if len(repoParts) != 2 { 77 writeError(w, xrpcerr.NewXrpcError( 78 xrpcerr.WithTag("InvalidRequest"), 79 xrpcerr.WithMessage("invalid repo format"), 80 ), http.StatusBadRequest) 81 return 82 } 83 qualifiedRepo, _ := securejoin.SecureJoin(repoParts[0], repoParts[1]) 84 pushOk, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, qualifiedRepo) 85 if err != nil || !pushOk { 86 writeError(w, xrpcerr.NewXrpcError( 87 xrpcerr.WithTag("Forbidden"), 88 xrpcerr.WithMessage("push permission required to apply changes"), 89 ), http.StatusForbidden) 90 return 91 } 92 93 // Open the repository with the target channel 94 pr, err := pijul.Open(repoPath, req.Channel) 95 if err != nil { 96 writeError(w, xrpcerr.NewXrpcError( 97 xrpcerr.WithTag("RepoNotFound"), 98 xrpcerr.WithMessage("failed to open pijul repository"), 99 ), http.StatusNotFound) 100 return 101 } 102 103 // Verify the channel exists 104 channels, err := pr.Channels() 105 if err != nil { 106 writeError(w, xrpcerr.NewXrpcError( 107 xrpcerr.WithTag("InternalServerError"), 108 xrpcerr.WithMessage("failed to list channels"), 109 ), http.StatusInternalServerError) 110 return 111 } 112 113 channelExists := false 114 for _, ch := range channels { 115 if ch.Name == req.Channel { 116 channelExists = true 117 break 118 } 119 } 120 121 if !channelExists { 122 writeError(w, xrpcerr.NewXrpcError( 123 xrpcerr.WithTag("ChannelNotFound"), 124 xrpcerr.WithMessage("target channel not found"), 125 ), http.StatusNotFound) 126 return 127 } 128 129 // Apply each change in order 130 response := ApplyChangesResponse{ 131 Applied: make([]string, 0), 132 Failed: make([]ApplyChangeFailure, 0), 133 } 134 135 for _, changeHash := range req.Changes { 136 if err := pr.Apply(changeHash); err != nil { 137 x.Logger.Error("failed to apply change", "hash", changeHash, "error", err.Error()) 138 response.Failed = append(response.Failed, ApplyChangeFailure{ 139 Hash: changeHash, 140 Error: err.Error(), 141 }) 142 } else { 143 response.Applied = append(response.Applied, changeHash) 144 x.Logger.Info("applied change", "hash", changeHash, "channel", req.Channel) 145 } 146 } 147 148 // If any changes failed, return partial success 149 if len(response.Failed) > 0 && len(response.Applied) == 0 { 150 writeError(w, xrpcerr.NewXrpcError( 151 xrpcerr.WithTag("ApplyFailed"), 152 xrpcerr.WithMessage("all changes failed to apply"), 153 ), http.StatusInternalServerError) 154 return 155 } 156 157 writeJson(w, response) 158}