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/db"
12 "tangled.org/core/knotserver/git"
13 "tangled.org/core/patchutil"
14 "tangled.org/core/rbac"
15 "tangled.org/core/tid"
16 "tangled.org/core/types"
17 xrpcerr "tangled.org/core/xrpc/errors"
18)
19
20func (x *Xrpc) Merge(w http.ResponseWriter, r *http.Request) {
21 l := x.Logger.With("handler", "Merge")
22 fail := func(e xrpcerr.XrpcError) {
23 l.Error("failed", "kind", e.Tag, "error", e.Message)
24 writeError(w, e, http.StatusBadRequest)
25 }
26
27 actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
28 if !ok {
29 fail(xrpcerr.MissingActorDidError)
30 return
31 }
32
33 var data tangled.RepoMerge_Input
34 if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
35 fail(xrpcerr.GenericError(err))
36 return
37 }
38
39 did := data.Did
40 name := data.Name
41
42 if did == "" || name == "" {
43 fail(xrpcerr.GenericError(fmt.Errorf("did and name are required")))
44 return
45 }
46
47 repoDid, err := x.Db.GetRepoDid(did, name)
48 if err != nil {
49 fail(xrpcerr.RepoNotFoundError)
50 return
51 }
52 repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid)
53 if err != nil {
54 fail(xrpcerr.RepoNotFoundError)
55 return
56 }
57
58 if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil {
59 l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid)
60 writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
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.CommitterName = x.Config.Git.UserName
85 mo.CommitterEmail = x.Config.Git.UserEmail
86 mo.FormatPatch = patchutil.IsFormatPatch(data.Patch)
87
88 err = gr.MergeWithOptions(data.Patch, data.Branch, mo)
89 if err != nil {
90 var mergeErr *git.ErrMerge
91 if errors.As(err, &mergeErr) {
92 conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
93 for i, conflict := range mergeErr.Conflicts {
94 conflicts[i] = types.ConflictInfo{
95 Filename: conflict.Filename,
96 Reason: conflict.Reason,
97 }
98 }
99
100 conflictErr := xrpcerr.NewXrpcError(
101 xrpcerr.WithTag("MergeConflict"),
102 xrpcerr.WithMessage(fmt.Sprintf("Merge failed due to conflicts: %s", mergeErr.Message)),
103 )
104 writeError(w, conflictErr, http.StatusConflict)
105 return
106 } else {
107 l.Error("failed to merge", "error", err.Error())
108 writeError(w, xrpcerr.GitError(err), http.StatusInternalServerError)
109 return
110 }
111 }
112
113 oldSha := gr.Hash()
114 if err := gr.Refresh(); err != nil {
115 l.Error("failed to refresh", "error", err)
116 }
117 newSha := gr.Hash()
118
119 go func() {
120 refUpdate := tangled.GitRefUpdate{
121 RepoDid: &repoDid,
122 OwnerDid: &data.Did,
123 RepoName: data.Name,
124 Ref: data.Branch,
125 OldSha: oldSha.String(),
126 NewSha: newSha.String(),
127 CommitterDid: actorDid.String(),
128 }
129 eventJson, err := json.Marshal(refUpdate)
130 if err != nil {
131 return
132 }
133
134 x.Db.InsertEvent(db.Event{
135 Rkey: tid.TID(),
136 Nsid: tangled.GitRefUpdateNSID,
137 EventJson: string(eventJson),
138 }, x.Notifier)
139 }()
140
141 w.WriteHeader(http.StatusOK)
142}