forked from
tangled.org/core
Monorepo for Tangled
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 Repo: f.RepoDidPtr(),
78 Branch: pull.TargetBranch,
79 Patch: patch,
80 CommitMessage: &pull.Title,
81 AuthorName: &authorName,
82 }
83
84 if pull.Body != "" {
85 mergeInput.CommitBody = &pull.Body
86 }
87
88 if email.Address != "" {
89 mergeInput.AuthorEmail = &email.Address
90 }
91
92 client, err := s.oauth.ServiceClient(
93 r,
94 oauth.WithService(f.Knot),
95 oauth.WithLxm(tangled.RepoMergeNSID),
96 oauth.WithDev(s.config.Core.Dev),
97 oauth.WithTimeout(time.Second*20), // merge is quite slow on large repos, like witchsky
98 )
99 if err != nil {
100 l.Error("failed to connect to knot server", "err", err, "knot", f.Knot)
101 s.pages.Notice(w, "pull-action-error", "Failed to merge pull request. Try again later.")
102 return
103 }
104
105 err = tangled.RepoMerge(r.Context(), client, mergeInput)
106 if xrpcerr := xrpcclient.HandleXrpcErr(err); xrpcerr != nil {
107 s.logger.Error("failed to merge", "xrpcerr", xrpcerr, "err", err)
108 s.pages.Notice(w, "pull-action-error", xrpcerr.Error())
109 return
110 }
111
112 var atUris []syntax.ATURI
113 for _, p := range pullsToMerge {
114 atUris = append(atUris, p.AtUri())
115 p.State = models.PullMerged
116 }
117
118 if err := s.writePullStatusRecords(r, user.Did, atUris, models.StateMerged); err != nil {
119 l.Error("failed to write pull status records after merge", "err", err)
120 }
121
122 tx, err := s.db.Begin()
123 if err != nil {
124 l.Error("failed to start transaction", "err", err)
125 s.pages.Notice(w, "pull-action-error", "Failed to merge pull request. Try again later.")
126 return
127 }
128 defer tx.Rollback()
129
130 err = db.MergePulls(tx, orm.FilterEq("repo_did", string(f.RepoDid)), orm.FilterIn("at_uri", atUris))
131 if err != nil {
132 l.Error("failed to update pull request status in database", "err", err)
133 s.pages.Notice(w, "pull-action-error", "Failed to merge pull request. Try again later.")
134 return
135 }
136
137 err = tx.Commit()
138 if err != nil {
139 // TODO: this is unsound, we should also revert the merge from the knotserver here
140 l.Error("failed to commit merge transaction", "err", err)
141 s.pages.Notice(w, "pull-action-error", "Failed to merge pull request. Try again later.")
142 return
143 }
144
145 // notify about the pull merge
146 for _, p := range pullsToMerge {
147 s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
148 }
149
150 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
151 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
152}