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