Gate issue and PR controls by policy
- SHA
810896d6852e77c122fd8d124477b4742b835b56- Parents
-
b9c83c2 - Tree
731fcd4
810896d
810896d6852e77c122fd8d124477b4742b835b56b9c83c2
731fcd4docs/internal/issues.mdmodified@@ -90,6 +90,15 @@ Issue creation and commenting follow GitHub's public-participation | |||
| 90 | model: any logged-in user may open or comment on issues in a public | 90 | model: any logged-in user may open or comment on issues in a public |
| 91 | repo, while private repos require `read` access. | 91 | repo, while private repos require `read` access. |
| 92 | 92 | ||
| 93 | +The issue page is capability-driven: | ||
| 94 | + | ||
| 95 | +- The comment box appears only when `policy.Can(issue:comment)` allows | ||
| 96 | + and the issue is not locked, unless the viewer is triage+. | ||
| 97 | +- Close/reopen appears for triage+ users and for the issue author. | ||
| 98 | +- Labels, assignees, milestones, and lock controls appear only for the | ||
| 99 | + matching triage-level policy action. Public participants should not | ||
| 100 | + see forms that only lead to 403s. | ||
| 101 | + | ||
| 93 | ## Cross-reference indexing | 102 | ## Cross-reference indexing |
| 94 | 103 | ||
| 95 | `internal/issues/references.go::insertReferencesFromBody` parses | 104 | `internal/issues/references.go::insertReferencesFromBody` parses |
docs/internal/pr-review.mdmodified@@ -85,6 +85,12 @@ without starting a review" path) is `pending=false` from the start, | |||
| 85 | The settings/branches handler now also accepts | 85 | The settings/branches handler now also accepts |
| 86 | `required_review_count` and `dismiss_stale_reviews_on_push`. | 86 | `required_review_count` and `dismiss_stale_reviews_on_push`. |
| 87 | 87 | ||
| 88 | +The PR template hides review-request, review-submit, inline-comment, | ||
| 89 | +and thread-resolve controls unless `policy.Can(pull:review)` allows | ||
| 90 | +the viewer. Merge and close controls are similarly driven by | ||
| 91 | +`pull:merge` and `pull:close` decisions. Public viewers who can read a | ||
| 92 | +PR should not see forms that only lead to 403s. | ||
| 93 | + | ||
| 88 | ## Required-review gate | 94 | ## Required-review gate |
| 89 | 95 | ||
| 90 | `internal/pulls/review/required.go::Evaluate` is the authoritative | 96 | `internal/pulls/review/required.go::Evaluate` is the authoritative |
internal/web/handlers/repo/issues.gomodified@@ -288,21 +288,37 @@ func (h *Handlers) issueView(w http.ResponseWriter, r *http.Request) { | |||
| 288 | authorName = u.Username | 288 | authorName = u.Username |
| 289 | } | 289 | } |
| 290 | } | 290 | } |
| 291 | + viewer := middleware.CurrentUserFromContext(r.Context()) | ||
| 292 | + actor := policy.UserActor(viewer.ID, viewer.Username, viewer.IsSuspended, false) | ||
| 293 | + pdeps := policy.Deps{Pool: h.d.Pool} | ||
| 294 | + repoRef := policy.NewRepoRefFromRepo(row) | ||
| 295 | + stateRef := repoRef | ||
| 296 | + if issue.AuthorUserID.Valid { | ||
| 297 | + stateRef.AuthorUserID = issue.AuthorUserID.Int64 | ||
| 298 | + } | ||
| 299 | + canCommentAction := policy.Can(r.Context(), pdeps, actor, policy.ActionIssueComment, repoRef).Allow | ||
| 300 | + canCommentThroughLock := policy.HasRoleAtLeast(r.Context(), pdeps, actor, repoRef, policy.RoleTriage) | ||
| 291 | 301 | ||
| 292 | w.Header().Set("Content-Type", "text/html; charset=utf-8") | 302 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| 293 | _ = h.d.Render.RenderPage(w, r, "repo/issue_view", map[string]any{ | 303 | _ = h.d.Render.RenderPage(w, r, "repo/issue_view", map[string]any{ |
| 294 | - "Title": issue.Title + " · " + row.Name, | 304 | + "Title": issue.Title + " · " + row.Name, |
| 295 | - "Owner": owner.Username, | 305 | + "Owner": owner.Username, |
| 296 | - "Repo": row, | 306 | + "Repo": row, |
| 297 | - "Issue": issue, | 307 | + "Issue": issue, |
| 298 | - "AuthorName": authorName, | 308 | + "AuthorName": authorName, |
| 299 | - "Comments": cs, | 309 | + "Comments": cs, |
| 300 | - "Events": events, | 310 | + "Events": events, |
| 301 | - "Labels": labels, | 311 | + "Labels": labels, |
| 302 | - "Assignees": assignees, | 312 | + "Assignees": assignees, |
| 303 | - "AllLabels": allLabels, | 313 | + "AllLabels": allLabels, |
| 304 | - "Milestones": milestones, | 314 | + "Milestones": milestones, |
| 305 | - "CSRFToken": middleware.CSRFTokenForRequest(r), | 315 | + "CanComment": canCommentAction && (!issue.Locked || canCommentThroughLock), |
| 316 | + "CanSetIssueState": policy.Can(r.Context(), pdeps, actor, policy.ActionIssueClose, stateRef).Allow, | ||
| 317 | + "CanEditIssueLabels": policy.Can(r.Context(), pdeps, actor, policy.ActionIssueLabel, repoRef).Allow, | ||
| 318 | + "CanEditIssueAssignees": policy.Can(r.Context(), pdeps, actor, policy.ActionIssueAssign, repoRef).Allow, | ||
| 319 | + "CanEditIssueMilestone": policy.Can(r.Context(), pdeps, actor, policy.ActionIssueLabel, repoRef).Allow, | ||
| 320 | + "CanLockIssue": policy.Can(r.Context(), pdeps, actor, policy.ActionIssueClose, repoRef).Allow, | ||
| 321 | + "CSRFToken": middleware.CSRFTokenForRequest(r), | ||
| 306 | }) | 322 | }) |
| 307 | } | 323 | } |
| 308 | 324 | ||
internal/web/handlers/repo/labels_milestones.gomodified@@ -24,13 +24,17 @@ func (h *Handlers) labelsList(w http.ResponseWriter, r *http.Request) { | |||
| 24 | return | 24 | return |
| 25 | } | 25 | } |
| 26 | labels, _ := h.iq.ListLabels(r.Context(), h.d.Pool, row.ID) | 26 | labels, _ := h.iq.ListLabels(r.Context(), h.d.Pool, row.ID) |
| 27 | + viewer := middleware.CurrentUserFromContext(r.Context()) | ||
| 28 | + actor := policy.UserActor(viewer.ID, viewer.Username, viewer.IsSuspended, false) | ||
| 29 | + canManage := policy.Can(r.Context(), policy.Deps{Pool: h.d.Pool}, actor, policy.ActionIssueLabel, policy.NewRepoRefFromRepo(row)).Allow | ||
| 27 | w.Header().Set("Content-Type", "text/html; charset=utf-8") | 30 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| 28 | _ = h.d.Render.RenderPage(w, r, "repo/labels", map[string]any{ | 31 | _ = h.d.Render.RenderPage(w, r, "repo/labels", map[string]any{ |
| 29 | - "Title": "Labels · " + row.Name, | 32 | + "Title": "Labels · " + row.Name, |
| 30 | - "Owner": owner.Username, | 33 | + "Owner": owner.Username, |
| 31 | - "Repo": row, | 34 | + "Repo": row, |
| 32 | - "Labels": labels, | 35 | + "Labels": labels, |
| 33 | - "CSRFToken": middleware.CSRFTokenForRequest(r), | 36 | + "CanManageIssue": canManage, |
| 37 | + "CSRFToken": middleware.CSRFTokenForRequest(r), | ||
| 34 | }) | 38 | }) |
| 35 | } | 39 | } |
| 36 | 40 | ||
@@ -119,13 +123,17 @@ func (h *Handlers) milestonesList(w http.ResponseWriter, r *http.Request) { | |||
| 119 | return | 123 | return |
| 120 | } | 124 | } |
| 121 | ms, _ := h.iq.ListMilestones(r.Context(), h.d.Pool, row.ID) | 125 | ms, _ := h.iq.ListMilestones(r.Context(), h.d.Pool, row.ID) |
| 126 | + viewer := middleware.CurrentUserFromContext(r.Context()) | ||
| 127 | + actor := policy.UserActor(viewer.ID, viewer.Username, viewer.IsSuspended, false) | ||
| 128 | + canManage := policy.Can(r.Context(), policy.Deps{Pool: h.d.Pool}, actor, policy.ActionIssueLabel, policy.NewRepoRefFromRepo(row)).Allow | ||
| 122 | w.Header().Set("Content-Type", "text/html; charset=utf-8") | 129 | w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| 123 | _ = h.d.Render.RenderPage(w, r, "repo/milestones", map[string]any{ | 130 | _ = h.d.Render.RenderPage(w, r, "repo/milestones", map[string]any{ |
| 124 | - "Title": "Milestones · " + row.Name, | 131 | + "Title": "Milestones · " + row.Name, |
| 125 | - "Owner": owner.Username, | 132 | + "Owner": owner.Username, |
| 126 | - "Repo": row, | 133 | + "Repo": row, |
| 127 | - "Milestones": ms, | 134 | + "Milestones": ms, |
| 128 | - "CSRFToken": middleware.CSRFTokenForRequest(r), | 135 | + "CanManageIssue": canManage, |
| 136 | + "CSRFToken": middleware.CSRFTokenForRequest(r), | ||
| 129 | }) | 137 | }) |
| 130 | } | 138 | } |
| 131 | 139 | ||
internal/web/handlers/repo/pulls.gomodified@@ -281,6 +281,18 @@ func (h *Handlers) renderPullPage(w http.ResponseWriter, r *http.Request, tab st | |||
| 281 | "Tab": tab, | 281 | "Tab": tab, |
| 282 | "CSRFToken": middleware.CSRFTokenForRequest(r), | 282 | "CSRFToken": middleware.CSRFTokenForRequest(r), |
| 283 | } | 283 | } |
| 284 | + viewer := middleware.CurrentUserFromContext(r.Context()) | ||
| 285 | + actor := policy.UserActor(viewer.ID, viewer.Username, viewer.IsSuspended, false) | ||
| 286 | + pdeps := policy.Deps{Pool: h.d.Pool} | ||
| 287 | + repoRef := policy.NewRepoRefFromRepo(row) | ||
| 288 | + stateRef := repoRef | ||
| 289 | + if pr.IAuthorUserID.Valid { | ||
| 290 | + stateRef.AuthorUserID = pr.IAuthorUserID.Int64 | ||
| 291 | + } | ||
| 292 | + data["CanReviewPull"] = policy.Can(r.Context(), pdeps, actor, policy.ActionPullReview, repoRef).Allow | ||
| 293 | + data["CanMergePull"] = policy.Can(r.Context(), pdeps, actor, policy.ActionPullMerge, repoRef).Allow | ||
| 294 | + data["CanSetPullState"] = policy.Can(r.Context(), pdeps, actor, policy.ActionPullClose, stateRef).Allow | ||
| 295 | + data["CanReadyPull"] = policy.Can(r.Context(), pdeps, actor, policy.ActionPullCreate, repoRef).Allow | ||
| 284 | for k, v := range extras { | 296 | for k, v := range extras { |
| 285 | data[k] = v | 297 | data[k] = v |
| 286 | } | 298 | } |
internal/web/templates/repo/issue_view.htmlmodified@@ -49,7 +49,7 @@ | |||
| 49 | </div> | 49 | </div> |
| 50 | {{ end }} | 50 | {{ end }} |
| 51 | 51 | ||
| 52 | - {{ if .Viewer.ID }} | 52 | + {{ if .CanComment }} |
| 53 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/comments" class="shithub-comment-form"> | 53 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/comments" class="shithub-comment-form"> |
| 54 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> | 54 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> |
| 55 | <label> | 55 | <label> |
@@ -57,14 +57,29 @@ | |||
| 57 | <textarea name="body" rows="6" maxlength="65535" required></textarea> | 57 | <textarea name="body" rows="6" maxlength="65535" required></textarea> |
| 58 | </label> | 58 | </label> |
| 59 | <div class="shithub-form-actions"> | 59 | <div class="shithub-form-actions"> |
| 60 | + {{ if .CanSetIssueState }} | ||
| 60 | {{ if eq (printf "%s" .Issue.State) "open" }} | 61 | {{ if eq (printf "%s" .Issue.State) "open" }} |
| 61 | <button type="submit" formaction="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/state" name="state" value="closed" class="shithub-button">Close issue</button> | 62 | <button type="submit" formaction="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/state" name="state" value="closed" class="shithub-button">Close issue</button> |
| 62 | {{ else }} | 63 | {{ else }} |
| 63 | <button type="submit" formaction="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/state" name="state" value="open" class="shithub-button">Reopen issue</button> | 64 | <button type="submit" formaction="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/state" name="state" value="open" class="shithub-button">Reopen issue</button> |
| 64 | {{ end }} | 65 | {{ end }} |
| 66 | + {{ end }} | ||
| 65 | <button type="submit" class="shithub-button shithub-button-primary">Comment</button> | 67 | <button type="submit" class="shithub-button shithub-button-primary">Comment</button> |
| 66 | </div> | 68 | </div> |
| 67 | </form> | 69 | </form> |
| 70 | + {{ else if .CanSetIssueState }} | ||
| 71 | + <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/state" class="shithub-comment-form"> | ||
| 72 | + <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> | ||
| 73 | + <div class="shithub-form-actions"> | ||
| 74 | + {{ if eq (printf "%s" .Issue.State) "open" }} | ||
| 75 | + <button type="submit" name="state" value="closed" class="shithub-button">Close issue</button> | ||
| 76 | + {{ else }} | ||
| 77 | + <button type="submit" name="state" value="open" class="shithub-button">Reopen issue</button> | ||
| 78 | + {{ end }} | ||
| 79 | + </div> | ||
| 80 | + </form> | ||
| 81 | + {{ else if .Viewer.ID }} | ||
| 82 | + {{ if .Issue.Locked }}<p class="shithub-issue-signedout">This conversation is locked.</p>{{ end }} | ||
| 68 | {{ else }} | 83 | {{ else }} |
| 69 | <p class="shithub-issue-signedout"><a href="/login">Sign in</a> to comment.</p> | 84 | <p class="shithub-issue-signedout"><a href="/login">Sign in</a> to comment.</p> |
| 70 | {{ end }} | 85 | {{ end }} |
@@ -76,7 +91,7 @@ | |||
| 76 | {{ if .Labels }} | 91 | {{ if .Labels }} |
| 77 | {{ range .Labels }}<span class="shithub-label" style="background-color: #{{ .Color }}">{{ .Name }}</span>{{ end }} | 92 | {{ range .Labels }}<span class="shithub-label" style="background-color: #{{ .Color }}">{{ .Name }}</span>{{ end }} |
| 78 | {{ else }}<p class="shithub-muted">None yet</p>{{ end }} | 93 | {{ else }}<p class="shithub-muted">None yet</p>{{ end }} |
| 79 | - {{ if .Viewer.ID }} | 94 | + {{ if .CanEditIssueLabels }} |
| 80 | <details> | 95 | <details> |
| 81 | <summary>Edit labels</summary> | 96 | <summary>Edit labels</summary> |
| 82 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/labels"> | 97 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/labels"> |
@@ -101,7 +116,7 @@ | |||
| 101 | {{ if .Assignees }} | 116 | {{ if .Assignees }} |
| 102 | {{ range .Assignees }}<a href="/{{ .Username }}">@{{ .Username }}</a>{{ end }} | 117 | {{ range .Assignees }}<a href="/{{ .Username }}">@{{ .Username }}</a>{{ end }} |
| 103 | {{ else }}<p class="shithub-muted">No one assigned</p>{{ end }} | 118 | {{ else }}<p class="shithub-muted">No one assigned</p>{{ end }} |
| 104 | - {{ if .Viewer.ID }} | 119 | + {{ if .CanEditIssueAssignees }} |
| 105 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/assignees" class="shithub-assignee-form"> | 120 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/assignees" class="shithub-assignee-form"> |
| 106 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> | 121 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> |
| 107 | <input type="text" name="username" placeholder="username" required> | 122 | <input type="text" name="username" placeholder="username" required> |
@@ -117,7 +132,7 @@ | |||
| 117 | {{ $mid := .Issue.MilestoneID.Int64 }} | 132 | {{ $mid := .Issue.MilestoneID.Int64 }} |
| 118 | {{ range .Milestones }}{{ if eq .ID $mid }}<a href="/{{ $.Owner }}/{{ $.Repo.Name }}/milestones">{{ .Title }}</a>{{ end }}{{ end }} | 133 | {{ range .Milestones }}{{ if eq .ID $mid }}<a href="/{{ $.Owner }}/{{ $.Repo.Name }}/milestones">{{ .Title }}</a>{{ end }}{{ end }} |
| 119 | {{ else }}<p class="shithub-muted">No milestone</p>{{ end }} | 134 | {{ else }}<p class="shithub-muted">No milestone</p>{{ end }} |
| 120 | - {{ if .Viewer.ID }} | 135 | + {{ if .CanEditIssueMilestone }} |
| 121 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/milestone"> | 136 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/milestone"> |
| 122 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> | 137 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> |
| 123 | <select name="milestone_id"> | 138 | <select name="milestone_id"> |
@@ -129,7 +144,7 @@ | |||
| 129 | {{ end }} | 144 | {{ end }} |
| 130 | </section> | 145 | </section> |
| 131 | 146 | ||
| 132 | - {{ if .Viewer.ID }} | 147 | + {{ if .CanLockIssue }} |
| 133 | <section> | 148 | <section> |
| 134 | <h3>Lock</h3> | 149 | <h3>Lock</h3> |
| 135 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/lock"> | 150 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/issues/{{ .Issue.Number }}/lock"> |
internal/web/templates/repo/labels.htmlmodified@@ -8,7 +8,7 @@ | |||
| 8 | </h1> | 8 | </h1> |
| 9 | </header> | 9 | </header> |
| 10 | 10 | ||
| 11 | - {{ if .Viewer.ID }} | 11 | + {{ if .CanManageIssue }} |
| 12 | <details class="shithub-label-create"> | 12 | <details class="shithub-label-create"> |
| 13 | <summary class="shithub-button shithub-button-primary">New label</summary> | 13 | <summary class="shithub-button shithub-button-primary">New label</summary> |
| 14 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/labels" class="shithub-label-form"> | 14 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/labels" class="shithub-label-form"> |
@@ -26,7 +26,7 @@ | |||
| 26 | <li class="shithub-labels-row"> | 26 | <li class="shithub-labels-row"> |
| 27 | <span class="shithub-label" style="background-color: #{{ .Color }}">{{ .Name }}</span> | 27 | <span class="shithub-label" style="background-color: #{{ .Color }}">{{ .Name }}</span> |
| 28 | {{ if .Description }}<span class="shithub-muted">{{ .Description }}</span>{{ end }} | 28 | {{ if .Description }}<span class="shithub-muted">{{ .Description }}</span>{{ end }} |
| 29 | - {{ if $.Viewer.ID }} | 29 | + {{ if $.CanManageIssue }} |
| 30 | <details class="shithub-label-edit"> | 30 | <details class="shithub-label-edit"> |
| 31 | <summary class="shithub-button">Edit</summary> | 31 | <summary class="shithub-button">Edit</summary> |
| 32 | <form method="post" action="/{{ $.Owner }}/{{ $.Repo.Name }}/labels/{{ .ID }}/update" class="shithub-label-form"> | 32 | <form method="post" action="/{{ $.Owner }}/{{ $.Repo.Name }}/labels/{{ .ID }}/update" class="shithub-label-form"> |
internal/web/templates/repo/milestones.htmlmodified@@ -8,7 +8,7 @@ | |||
| 8 | </h1> | 8 | </h1> |
| 9 | </header> | 9 | </header> |
| 10 | 10 | ||
| 11 | - {{ if .Viewer.ID }} | 11 | + {{ if .CanManageIssue }} |
| 12 | <details class="shithub-milestone-create"> | 12 | <details class="shithub-milestone-create"> |
| 13 | <summary class="shithub-button shithub-button-primary">New milestone</summary> | 13 | <summary class="shithub-button shithub-button-primary">New milestone</summary> |
| 14 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/milestones" class="shithub-milestone-form"> | 14 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/milestones" class="shithub-milestone-form"> |
@@ -29,7 +29,7 @@ | |||
| 29 | {{ if .DueOn.Valid }}<small>due {{ .DueOn.Time.Format "Jan 2, 2006" }}</small>{{ end }} | 29 | {{ if .DueOn.Valid }}<small>due {{ .DueOn.Time.Format "Jan 2, 2006" }}</small>{{ end }} |
| 30 | </h3> | 30 | </h3> |
| 31 | {{ if .Description }}<p>{{ .Description }}</p>{{ end }} | 31 | {{ if .Description }}<p>{{ .Description }}</p>{{ end }} |
| 32 | - {{ if $.Viewer.ID }} | 32 | + {{ if $.CanManageIssue }} |
| 33 | <div class="shithub-milestone-actions"> | 33 | <div class="shithub-milestone-actions"> |
| 34 | <form method="post" action="/{{ $.Owner }}/{{ $.Repo.Name }}/milestones/{{ .ID }}/state"> | 34 | <form method="post" action="/{{ $.Owner }}/{{ $.Repo.Name }}/milestones/{{ .ID }}/state"> |
| 35 | <input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}"> | 35 | <input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}"> |
internal/web/templates/repo/pull_view.htmlmodified@@ -96,7 +96,7 @@ | |||
| 96 | </section> | 96 | </section> |
| 97 | {{ end }} | 97 | {{ end }} |
| 98 | 98 | ||
| 99 | - {{ if .Viewer.ID }} | 99 | + {{ if .CanReviewPull }} |
| 100 | <details class="shithub-pull-request-reviewer"> | 100 | <details class="shithub-pull-request-reviewer"> |
| 101 | <summary class="shithub-button">Request review</summary> | 101 | <summary class="shithub-button">Request review</summary> |
| 102 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/pulls/{{ .PR.INumber }}/reviewers"> | 102 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/pulls/{{ .PR.INumber }}/reviewers"> |
@@ -136,7 +136,8 @@ | |||
| 136 | <p class="shithub-pull-state-{{ printf "%s" .PR.MergeableState }}"> | 136 | <p class="shithub-pull-state-{{ printf "%s" .PR.MergeableState }}"> |
| 137 | {{ printf "%s" .PR.MergeableState }} | 137 | {{ printf "%s" .PR.MergeableState }} |
| 138 | </p> | 138 | </p> |
| 139 | - {{ if .Viewer.ID }} | 139 | + {{ if or .CanMergePull .CanSetPullState .CanReadyPull }} |
| 140 | + {{ if .CanMergePull }} | ||
| 140 | {{ if eq (printf "%s" .PR.MergeableState) "clean" }} | 141 | {{ if eq (printf "%s" .PR.MergeableState) "clean" }} |
| 141 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/pulls/{{ .PR.INumber }}/merge" class="shithub-pull-merge-form"> | 142 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/pulls/{{ .PR.INumber }}/merge" class="shithub-pull-merge-form"> |
| 142 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> | 143 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> |
@@ -154,11 +155,14 @@ | |||
| 154 | {{ else }} | 155 | {{ else }} |
| 155 | <p class="shithub-muted">Mergeability is being computed…</p> | 156 | <p class="shithub-muted">Mergeability is being computed…</p> |
| 156 | {{ end }} | 157 | {{ end }} |
| 158 | + {{ end }} | ||
| 159 | + {{ if .CanSetPullState }} | ||
| 157 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/pulls/{{ .PR.INumber }}/state" class="shithub-pull-state-form"> | 160 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/pulls/{{ .PR.INumber }}/state" class="shithub-pull-state-form"> |
| 158 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> | 161 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> |
| 159 | <button type="submit" name="state" value="closed" class="shithub-button">Close pull request</button> | 162 | <button type="submit" name="state" value="closed" class="shithub-button">Close pull request</button> |
| 160 | </form> | 163 | </form> |
| 161 | - {{ if .PR.Draft }} | 164 | + {{ end }} |
| 165 | + {{ if and .CanReadyPull .PR.Draft }} | ||
| 162 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/pulls/{{ .PR.INumber }}/ready"> | 166 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/pulls/{{ .PR.INumber }}/ready"> |
| 163 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> | 167 | <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}"> |
| 164 | <button type="submit" class="shithub-button">Ready for review</button> | 168 | <button type="submit" class="shithub-button">Ready for review</button> |
@@ -206,7 +210,7 @@ | |||
| 206 | <div class="shithub-comment-body markdown-body"> | 210 | <div class="shithub-comment-body markdown-body"> |
| 207 | {{ if .C.BodyHtmlCached.Valid }}{{ safeHTML .C.BodyHtmlCached.String }}{{ else }}<p>{{ .C.Body }}</p>{{ end }} | 211 | {{ if .C.BodyHtmlCached.Valid }}{{ safeHTML .C.BodyHtmlCached.String }}{{ else }}<p>{{ .C.Body }}</p>{{ end }} |
| 208 | </div> | 212 | </div> |
| 209 | - {{ if $.Viewer.ID }} | 213 | + {{ if $.CanReviewPull }} |
| 210 | <div class="shithub-pull-thread-actions"> | 214 | <div class="shithub-pull-thread-actions"> |
| 211 | <form method="post" action="/{{ $.Owner }}/{{ $.Repo.Name }}/pulls/{{ $.PR.INumber }}/review-comments/{{ .C.ID }}/reply"> | 215 | <form method="post" action="/{{ $.Owner }}/{{ $.Repo.Name }}/pulls/{{ $.PR.INumber }}/review-comments/{{ .C.ID }}/reply"> |
| 212 | <input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}"> | 216 | <input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}"> |
@@ -225,7 +229,7 @@ | |||
| 225 | {{ end }} | 229 | {{ end }} |
| 226 | {{ end }} | 230 | {{ end }} |
| 227 | 231 | ||
| 228 | - {{ if .Viewer.ID }} | 232 | + {{ if .CanReviewPull }} |
| 229 | <details class="shithub-pull-add-comment"> | 233 | <details class="shithub-pull-add-comment"> |
| 230 | <summary class="shithub-button">Add inline comment</summary> | 234 | <summary class="shithub-button">Add inline comment</summary> |
| 231 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/pulls/{{ .PR.INumber }}/review-comments"> | 235 | <form method="post" action="/{{ .Owner }}/{{ .Repo.Name }}/pulls/{{ .PR.INumber }}/review-comments"> |