···11+package xrpc22+33+import (44+ "encoding/json"55+ "errors"66+ "fmt"77+ "net/http"88+99+ "github.com/bluesky-social/indigo/atproto/syntax"1010+ securejoin "github.com/cyphar/filepath-securejoin"1111+ "tangled.sh/tangled.sh/core/api/tangled"1212+ "tangled.sh/tangled.sh/core/knotserver/git"1313+ "tangled.sh/tangled.sh/core/patchutil"1414+ "tangled.sh/tangled.sh/core/rbac"1515+ "tangled.sh/tangled.sh/core/types"1616+ xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"1717+)1818+1919+func (x *Xrpc) Merge(w http.ResponseWriter, r *http.Request) {2020+ l := x.Logger.With("handler", "Merge")2121+ fail := func(e xrpcerr.XrpcError) {2222+ l.Error("failed", "kind", e.Tag, "error", e.Message)2323+ writeError(w, e, http.StatusBadRequest)2424+ }2525+2626+ actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)2727+ if !ok {2828+ fail(xrpcerr.MissingActorDidError)2929+ return3030+ }3131+3232+ var data tangled.RepoMerge_Input3333+ if err := json.NewDecoder(r.Body).Decode(&data); err != nil {3434+ fail(xrpcerr.GenericError(err))3535+ return3636+ }3737+3838+ did := data.Did3939+ name := data.Name4040+4141+ if did == "" || name == "" {4242+ fail(xrpcerr.GenericError(fmt.Errorf("did and name are required")))4343+ return4444+ }4545+4646+ relativeRepoPath, err := securejoin.SecureJoin(did, name)4747+ if err != nil {4848+ fail(xrpcerr.GenericError(err))4949+ return5050+ }5151+5252+ if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil {5353+ l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath)5454+ writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)5555+ return5656+ }5757+5858+ repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath)5959+ if err != nil {6060+ fail(xrpcerr.GenericError(err))6161+ return6262+ }6363+6464+ gr, err := git.Open(repoPath, data.Branch)6565+ if err != nil {6666+ fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err)))6767+ return6868+ }6969+7070+ mo := &git.MergeOptions{}7171+ if data.AuthorName != nil {7272+ mo.AuthorName = *data.AuthorName7373+ }7474+ if data.AuthorEmail != nil {7575+ mo.AuthorEmail = *data.AuthorEmail7676+ }7777+ if data.CommitBody != nil {7878+ mo.CommitBody = *data.CommitBody7979+ }8080+ if data.CommitMessage != nil {8181+ mo.CommitMessage = *data.CommitMessage8282+ }8383+8484+ mo.FormatPatch = patchutil.IsFormatPatch(data.Patch)8585+8686+ err = gr.MergeWithOptions([]byte(data.Patch), data.Branch, mo)8787+ if err != nil {8888+ var mergeErr *git.ErrMerge8989+ if errors.As(err, &mergeErr) {9090+ conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))9191+ for i, conflict := range mergeErr.Conflicts {9292+ conflicts[i] = types.ConflictInfo{9393+ Filename: conflict.Filename,9494+ Reason: conflict.Reason,9595+ }9696+ }9797+9898+ conflictErr := xrpcerr.NewXrpcError(9999+ xrpcerr.WithTag("MergeConflict"),100100+ xrpcerr.WithMessage(fmt.Sprintf("Merge failed due to conflicts: %s", mergeErr.Message)),101101+ )102102+ writeError(w, conflictErr, http.StatusConflict)103103+ return104104+ } else {105105+ l.Error("failed to merge", "error", err.Error())106106+ writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError)107107+ return108108+ }109109+ }110110+111111+ w.WriteHeader(http.StatusOK)112112+}
+87
knotserver/xrpc/merge_check.go
···11+package xrpc22+33+import (44+ "encoding/json"55+ "errors"66+ "fmt"77+ "net/http"88+99+ securejoin "github.com/cyphar/filepath-securejoin"1010+ "tangled.sh/tangled.sh/core/api/tangled"1111+ "tangled.sh/tangled.sh/core/knotserver/git"1212+ xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"1313+)1414+1515+func (x *Xrpc) MergeCheck(w http.ResponseWriter, r *http.Request) {1616+ l := x.Logger.With("handler", "MergeCheck")1717+ fail := func(e xrpcerr.XrpcError) {1818+ l.Error("failed", "kind", e.Tag, "error", e.Message)1919+ writeError(w, e, http.StatusBadRequest)2020+ }2121+2222+ var data tangled.RepoMergeCheck_Input2323+ if err := json.NewDecoder(r.Body).Decode(&data); err != nil {2424+ fail(xrpcerr.GenericError(err))2525+ return2626+ }2727+2828+ did := data.Did2929+ name := data.Name3030+3131+ if did == "" || name == "" {3232+ fail(xrpcerr.GenericError(fmt.Errorf("did and name are required")))3333+ return3434+ }3535+3636+ relativeRepoPath, err := securejoin.SecureJoin(did, name)3737+ if err != nil {3838+ fail(xrpcerr.GenericError(err))3939+ return4040+ }4141+4242+ repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath)4343+ if err != nil {4444+ fail(xrpcerr.GenericError(err))4545+ return4646+ }4747+4848+ gr, err := git.Open(repoPath, data.Branch)4949+ if err != nil {5050+ fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err)))5151+ return5252+ }5353+5454+ err = gr.MergeCheck([]byte(data.Patch), data.Branch)5555+5656+ response := tangled.RepoMergeCheck_Output{5757+ Is_conflicted: false,5858+ }5959+6060+ if err != nil {6161+ var mergeErr *git.ErrMerge6262+ if errors.As(err, &mergeErr) {6363+ response.Is_conflicted = true6464+6565+ conflicts := make([]*tangled.RepoMergeCheck_ConflictInfo, len(mergeErr.Conflicts))6666+ for i, conflict := range mergeErr.Conflicts {6767+ conflicts[i] = &tangled.RepoMergeCheck_ConflictInfo{6868+ Filename: conflict.Filename,6969+ Reason: conflict.Reason,7070+ }7171+ }7272+ response.Conflicts = conflicts7373+7474+ if mergeErr.Message != "" {7575+ response.Message = &mergeErr.Message7676+ }7777+ } else {7878+ response.Is_conflicted = true7979+ errMsg := err.Error()8080+ response.Error = &errMsg8181+ }8282+ }8383+8484+ w.Header().Set("Content-Type", "application/json")8585+ w.WriteHeader(http.StatusOK)8686+ json.NewEncoder(w).Encode(response)8787+}
+13-4
knotserver/xrpc/router.go
···31313232func (x *Xrpc) Router() http.Handler {3333 r := chi.NewRouter()3434+ r.Group(func(r chi.Router) {3535+ r.Use(x.ServiceAuth.VerifyServiceAuth)34363535- r.With(x.ServiceAuth.VerifyServiceAuth).Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch)3737+ r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch)3838+ r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo)3939+ r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo)4040+ r.Post("/"+tangled.RepoForkNSID, x.ForkRepo)4141+ r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus)4242+ r.Post("/"+tangled.RepoForkSyncNSID, x.ForkSync)36434444+ r.Post("/"+tangled.RepoHiddenRefNSID, x.HiddenRef)4545+4646+ r.Post("/"+tangled.RepoMergeNSID, x.Merge)4747+ r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck)4848+ })3749 return r3850}39514040-// this is slightly different from http_util::write_error to follow the spec:4141-//4242-// the json object returned must include an "error" and a "message"4352func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {4453 w.Header().Set("Content-Type", "application/json")4554 w.WriteHeader(status)