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