Monorepo for Tangled
tangled.org
1package pulls
2
3import (
4 "fmt"
5 "net/http"
6
7 "tangled.org/core/appview/db"
8 "tangled.org/core/appview/models"
9 "tangled.org/core/appview/reporesolver"
10 "tangled.org/core/orm"
11
12 "github.com/bluesky-social/indigo/atproto/syntax"
13)
14
15func (s *Pulls) ClosePull(w http.ResponseWriter, r *http.Request) {
16 l := s.logger.With("handler", "ClosePull")
17
18 user := s.oauth.GetMultiAccountUser(r)
19 if user != nil {
20 l = l.With("user", user.Did)
21 }
22
23 f, err := s.repoResolver.Resolve(r)
24 if err != nil {
25 l.Error("failed to resolve repo", "err", err)
26 return
27 }
28
29 pull, ok := r.Context().Value("pull").(*models.Pull)
30 if !ok {
31 l.Error("failed to get pull")
32 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
33 return
34 }
35 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid)
36
37 // auth filter: only owner or collaborators can close
38 roles := s.acl.RolesInRepo(r.Context(), f, user.Did)
39 isOwner := roles.IsOwner()
40 isCollaborator := roles.IsCollaborator()
41 isPullAuthor := user.Did == pull.OwnerDid
42 isCloseAllowed := isOwner || isCollaborator || isPullAuthor
43 if !isCloseAllowed {
44 l.Error("unauthorized to close pull", "is_owner", isOwner, "is_collaborator", isCollaborator, "is_pull_author", isPullAuthor)
45 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
46 return
47 }
48
49 // Start a transaction
50 tx, err := s.db.BeginTx(r.Context(), nil)
51 if err != nil {
52 l.Error("failed to start transaction", "err", err)
53 s.pages.Notice(w, "pull-close", "Failed to close pull.")
54 return
55 }
56 defer tx.Rollback()
57
58 // if this PR is stacked, then we want to close all PRs above this one on the stack
59 stack := r.Context().Value("stack").(models.Stack)
60 pullsToClose := stack.Above(pull)
61 var atUris []syntax.ATURI
62 for _, p := range pullsToClose {
63 atUris = append(atUris, p.AtUri())
64 p.State = models.PullClosed
65 }
66 err = db.ClosePulls(
67 tx,
68 orm.FilterEq("repo_did", string(f.RepoDid)),
69 orm.FilterIn("at_uri", atUris),
70 )
71 if err != nil {
72 l.Error("failed to close pulls in database", "err", err, "pulls_to_close", len(pullsToClose))
73 s.pages.Notice(w, "pull-close", "Failed to close pull.")
74 }
75
76 // Commit the transaction
77 if err = tx.Commit(); err != nil {
78 l.Error("failed to commit transaction", "err", err)
79 s.pages.Notice(w, "pull-close", "Failed to close pull.")
80 return
81 }
82
83 for _, p := range pullsToClose {
84 s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
85 }
86
87 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
88 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
89}
90
91func (s *Pulls) ReopenPull(w http.ResponseWriter, r *http.Request) {
92 l := s.logger.With("handler", "ReopenPull")
93
94 user := s.oauth.GetMultiAccountUser(r)
95 if user != nil {
96 l = l.With("user", user.Did)
97 }
98
99 f, err := s.repoResolver.Resolve(r)
100 if err != nil {
101 l.Error("failed to resolve repo", "err", err)
102 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
103 return
104 }
105
106 pull, ok := r.Context().Value("pull").(*models.Pull)
107 if !ok {
108 l.Error("failed to get pull")
109 s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
110 return
111 }
112 l = l.With("pull_id", pull.PullId, "pull_owner", pull.OwnerDid, "state", pull.State)
113
114 // auth filter: only owner or collaborators can close
115 roles := s.acl.RolesInRepo(r.Context(), f, user.Did)
116 isOwner := roles.IsOwner()
117 isCollaborator := roles.IsCollaborator()
118 isPullAuthor := user.Did == pull.OwnerDid
119 isCloseAllowed := isOwner || isCollaborator || isPullAuthor
120 if !isCloseAllowed {
121 l.Error("unauthorized to reopen pull", "is_owner", isOwner, "is_collaborator", isCollaborator, "is_pull_author", isPullAuthor)
122 s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
123 return
124 }
125
126 // Start a transaction
127 tx, err := s.db.BeginTx(r.Context(), nil)
128 if err != nil {
129 l.Error("failed to start transaction", "err", err)
130 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
131 return
132 }
133 defer tx.Rollback()
134
135 // if this PR is stacked, then we want to reopen all PRs above this one on the stack
136 stack := r.Context().Value("stack").(models.Stack)
137 pullsToReopen := stack.Below(pull)
138 var atUris []syntax.ATURI
139 for _, p := range pullsToReopen {
140 atUris = append(atUris, p.AtUri())
141 p.State = models.PullOpen
142 }
143 err = db.ReopenPulls(
144 tx,
145 orm.FilterEq("repo_did", string(f.RepoDid)),
146 orm.FilterIn("at_uri", atUris),
147 )
148 if err != nil {
149 l.Error("failed to reopen pulls in database", "err", err, "pulls_to_reopen", len(pullsToReopen))
150 s.pages.Notice(w, "pull-close", "Failed to reopen pull.")
151 }
152
153 // Commit the transaction
154 if err = tx.Commit(); err != nil {
155 l.Error("failed to commit transaction", "err", err)
156 s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
157 return
158 }
159
160 for _, p := range pullsToReopen {
161 s.notifier.NewPullState(r.Context(), syntax.DID(user.Did), p)
162 }
163
164 ownerSlashRepo := reporesolver.GetBaseRepoPath(r, f)
165 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", ownerSlashRepo, pull.PullId))
166}