Monorepo for Tangled
tangled.org
1package pulls
2
3import (
4 "fmt"
5 "net/http"
6 "time"
7
8 "tangled.org/core/api/tangled"
9 "tangled.org/core/appview/db"
10 "tangled.org/core/appview/models"
11 "tangled.org/core/appview/oauth"
12 "tangled.org/core/appview/reporesolver"
13 "tangled.org/core/orm"
14 "tangled.org/core/xrpc/xrpcclient"
15
16 "github.com/bluesky-social/indigo/atproto/syntax"
17)
18
19func (s *Pulls) MergePull(w http.ResponseWriter, r *http.Request) {
20 l := s.logger.With("handler", "MergePull")
21
22 user := s.oauth.GetMultiAccountUser(r)
23 if user == nil {
24 l.Error("nil user")
25 s.pages.Notice(w, "pull-action-error", "You must be logged in to merge this pull.")
26 return
27 }
28 l = l.With("user", user.Did)
29
30 f, err := s.repoResolver.Resolve(r)
31 if err != nil {
32 l.Error("failed to resolve repo", "err", err)
33 s.pages.Notice(w, "pull-action-error", "Failed to merge pull request. Try again later.")
34 return
35 }
36 l = l.With("repo_at", f.RepoAt().String())
37
38 pull, ok := r.Context().Value("pull").(*models.Pull)
39 if !ok {
40 l.Error("failed to get pull")
41 s.pages.Notice(w, "pull-action-error", "Failed to merge patch. Try again later.")
42 return
43 }
44 l = l.With("pull_id", pull.PullId, "target_branch", pull.TargetBranch)
45
46 stack, ok := r.Context().Value("stack").(models.Stack)
47 if !ok {
48 l.Error("failed to get stack")
49 s.pages.Notice(w, "pull-action-error", "Failed to merge patch. Try again later.")
50 return
51 }
52
53 // combine patches of substack
54 subStack := stack.Below(pull)
55 // collect the portion of the stack that is mergeable
56 pullsToMerge := subStack.Mergeable()
57 l = l.With("pulls_to_merge", len(pullsToMerge))
58
59 patch := pullsToMerge.CombinedPatch()
60
61 ident, err := s.idResolver.ResolveIdent(r.Context(), pull.OwnerDid)
62 if err != nil {
63 l.Error("failed to resolve identity", "err", err, "owner_did", pull.OwnerDid)
64 w.WriteHeader(http.StatusNotFound)
65 return
66 }
67
68 email, err := db.GetPrimaryEmail(s.db, pull.OwnerDid)
69 if err != nil {
70 l.Warn("failed to get primary email", "err", err, "owner_did", pull.OwnerDid)
71 }
72
73 authorName := ident.Handle.String()
74 mergeInput := &tangled.RepoMerge_Input{
75 Did: f.Did,
76 Name: f.Name,
77 Branch: pull.TargetBranch,
78 Patch: patch,
79 CommitMessage: &pull.Title,
80 AuthorName: &authorName,
81 }
82
83 if pull.Body != "" {
84 mergeInput.CommitBody = &pull.Body
85 }
86
87 if email.Address != "" {
88 mergeInput.AuthorEmail = &email.Address
89 }
90
91 client, err := s.oauth.ServiceClient(
92 r,
93 oauth.WithService(f.Knot),
94 oauth.WithLxm(tangled.RepoMergeNSID),
95 oauth.WithDev(s.config.Core.Dev),
96 oauth.WithTimeout(time.Second*20), // merge is quite slow on large repos, like witchsky
97 )
98 if err != nil {
99 l.Error("failed to connect to knot server", "err", err, "knot", f.Knot)
100 s.pages.Notice(w, "pull-action-error", "Failed to merge pull request. Try again later.")
101 return
102 }
103
104 err = tangled.RepoMerge(r.Context(), client, mergeInput)
105 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
106 s.logger.Error("failed to merge", "xrpcerr", xrpcerr, "err", err)
107 s.pages.Notice(w, "pull-action-error", xrpcerr.Error())
108 return
109 }
110
111 var atUris []syntax.ATURI
112 for _, p := range pullsToMerge {
113 atUris = append(atUris, p.AtUri())
114 p.State = models.PullMerged
115 }
116
117 if err := s.writePullStatusRecords(r, user.Did, atUris, models.StateMerged); err != nil {
118 l.Error("failed to write pull status records after merge", "err", err)
119 }
120
121 tx, err := s.db.Begin()
122 if err != nil {
123 l.Error("failed to start transaction", "err", err)
124 s.pages.Notice(w, "pull-action-error", "Failed to merge pull request. Try again later.")
125 return
126 }
127 defer tx.Rollback()
128
129 err = db.MergePulls(tx, orm.FilterEq("repo_did", string(f.RepoDid)), orm.FilterIn("at_uri", atUris))
130 if err != nil {
131 l.Error("failed to update pull request status in database", "err", err)
132 s.pages.Notice(w, "pull-action-error", "Failed to merge pull request. Try again later.")
133 return
134 }
135
136 err = tx.Commit()
137 if err != nil {
138 // TODO: this is unsound, we should also revert the merge from the knotserver here
139 l.Error("failed to commit merge transaction", "err", err)
140 s.pages.Notice(w, "pull-action-error", "Failed to merge pull request. Try again later.")
141 return
142 }
143
144 // notify about the pull merge
145 for _, p := range pullsToMerge {
146 s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
147 }
148
149 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
150 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
151}