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