forked from
tangled.org/core
Monorepo for Tangled — https://tangled.org
1package xrpc
2
3import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "net/http"
8
9 "github.com/bluesky-social/indigo/atproto/syntax"
10 securejoin "github.com/cyphar/filepath-securejoin"
11 "tangled.sh/tangled.sh/core/api/tangled"
12 "tangled.sh/tangled.sh/core/knotserver/git"
13 "tangled.sh/tangled.sh/core/patchutil"
14 "tangled.sh/tangled.sh/core/rbac"
15 "tangled.sh/tangled.sh/core/types"
16 xrpcerr "tangled.sh/tangled.sh/core/xrpc/errors"
17)
18
19func (x *Xrpc) Merge(w http.ResponseWriter, r *http.Request) {
20 l := x.Logger.With("handler", "Merge")
21 fail := func(e xrpcerr.XrpcError) {
22 l.Error("failed", "kind", e.Tag, "error", e.Message)
23 writeError(w, e, http.StatusBadRequest)
24 }
25
26 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
27 if !ok {
28 fail(xrpcerr.MissingActorDidError)
29 return
30 }
31
32 var data tangled.RepoMerge_Input
33 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
34 fail(xrpcerr.GenericError(err))
35 return
36 }
37
38 did := data.Did
39 name := data.Name
40
41 if did == "" || name == "" {
42 fail(xrpcerr.GenericError(fmt.Errorf("did and name are required")))
43 return
44 }
45
46 relativeRepoPath, err := securejoin.SecureJoin(did, name)
47 if err != nil {
48 fail(xrpcerr.GenericError(err))
49 return
50 }
51
52 if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil {
53 l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath)
54 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
55 return
56 }
57
58 repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath)
59 if err != nil {
60 fail(xrpcerr.GenericError(err))
61 return
62 }
63
64 gr, err := git.Open(repoPath, data.Branch)
65 if err != nil {
66 fail(xrpcerr.GenericError(fmt.Errorf("failed to open repository: %w", err)))
67 return
68 }
69
70 mo := &git.MergeOptions{}
71 if data.AuthorName != nil {
72 mo.AuthorName = *data.AuthorName
73 }
74 if data.AuthorEmail != nil {
75 mo.AuthorEmail = *data.AuthorEmail
76 }
77 if data.CommitBody != nil {
78 mo.CommitBody = *data.CommitBody
79 }
80 if data.CommitMessage != nil {
81 mo.CommitMessage = *data.CommitMessage
82 }
83
84 mo.FormatPatch = patchutil.IsFormatPatch(data.Patch)
85
86 err = gr.MergeWithOptions([]byte(data.Patch), data.Branch, mo)
87 if err != nil {
88 var mergeErr *git.ErrMerge
89 if errors.As(err, &mergeErr) {
90 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
91 for i, conflict := range mergeErr.Conflicts {
92 conflicts[i] = types.ConflictInfo{
93 Filename: conflict.Filename,
94 Reason: conflict.Reason,
95 }
96 }
97
98 conflictErr := xrpcerr.NewXrpcError(
99 xrpcerr.WithTag("MergeConflict"),
100 xrpcerr.WithMessage(fmt.Sprintf("Merge failed due to conflicts: %s", mergeErr.Message)),
101 )
102 writeError(w, conflictErr, http.StatusConflict)
103 return
104 } else {
105 l.Error("failed to merge", "error", err.Error())
106 writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError)
107 return
108 }
109 }
110
111 w.WriteHeader(http.StatusOK)
112}