forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

add acls to patch-requests, refactor

Changed files
+348 -328
appview
db
pages
templates
repo
pulls
state
+6 -3
appview/db/pulls.go
···
func GetPullCount(e Execer, repoAt syntax.ATURI) (PullCount, error) {
row := e.QueryRow(`
select
-
count(case when state = 0 then 1 end) as open_count,
-
count(case when state = 1 then 1 end) as merged_count,
-
count(case when state = 2 then 1 end) as closed_count
from pulls
where repo_at = ?`,
repoAt,
)
···
func GetPullCount(e Execer, repoAt syntax.ATURI) (PullCount, error) {
row := e.QueryRow(`
select
+
count(case when state = ? then 1 end) as open_count,
+
count(case when state = ? then 1 end) as merged_count,
+
count(case when state = ? then 1 end) as closed_count
from pulls
where repo_at = ?`,
+
PullOpen,
+
PullMerged,
+
PullClosed,
repoAt,
)
+197 -236
appview/pages/templates/repo/pulls/pull.html
···
></i>
<span class="text-white">{{ .Pull.State.String }}</span>
</div>
-
<span class="text-gray-400 text-sm">
opened by
-
{{ $owner := didOrHandle .Pull.OwnerDid .PullOwnerHandle }}
<a href="/{{ $owner }}" class="no-underline hover:underline"
>{{ $owner }}</a
>
···
id="patch"
name="patch"
class="font-mono w-full h-full p-4 rounded-b border border-gray-200 text-sm hidden"
-
>
-
{{- .Pull.Patch -}}</textarea
-
>
<div class="flex gap-2 justify-end mt-2">
<button
···
{{ end }}
{{ define "repoAfter" }}
<section id="comments" class="mt-8 space-y-4 relative">
-
{{ range $index, $comment := .Comments }}
-
<div
-
id="comment-{{ .CommentId }}"
-
class="rounded bg-white p-4 relative"
-
>
-
{{ if eq $index 0 }}
-
<div
-
class="absolute left-8 -top-8 w-px h-8 bg-gray-300"
-
></div>
-
{{ else }}
-
<div
-
class="absolute left-8 -top-4 w-px h-4 bg-gray-300"
-
></div>
-
{{ end }}
-
<div class="flex items-center gap-2 mb-2 text-gray-400">
-
{{ $owner := index $.DidHandleMap .OwnerDid }}
-
<span class="text-sm">
-
<a
-
href="/{{ $owner }}"
-
class="no-underline hover:underline"
-
>{{ $owner }}</a
-
>
-
</span>
-
<span
-
class="px-1 select-none before:content-['\00B7']"
-
></span>
-
<a
-
href="#{{ .CommentId }}"
-
class="text-gray-500 text-sm hover:text-gray-500 hover:underline no-underline"
-
id="{{ .CommentId }}"
-
>
-
{{ .Created | timeFmt }}
-
</a>
-
</div>
-
<div class="prose">
-
{{ .Body | markdown }}
-
</div>
-
</div>
-
{{ end }}
-
{{ if .Pull.State.IsMerged }}
-
<div
-
id="merge-status-card"
-
class="rounded relative bg-purple-50 border border-purple-200 p-4"
-
>
-
{{ if gt (len .Comments) 0 }}
-
<div
-
class="absolute left-8 -top-4 w-px h-4 bg-gray-300"
-
></div>
-
{{ else }}
-
<div
-
class="absolute left-8 -top-8 w-px h-8 bg-gray-300"
-
></div>
-
{{ end }}
-
-
-
<div class="flex items-center gap-2 text-purple-500">
-
<i data-lucide="git-merge" class="w-4 h-4"></i>
-
<span class="font-medium"
-
>Pull request successfully merged</span
-
>
-
</div>
-
-
<div class="mt-2 text-sm text-gray-700">
-
<p>
-
This pull request has been merged into the base branch.
-
</p>
-
</div>
-
</div>
-
{{ else if .MergeCheck }}
-
<div
-
id="merge-status-card"
-
class="rounded relative {{ if .MergeCheck.IsConflicted }}
-
bg-red-50 border border-red-200
-
{{ else }}
-
bg-green-50 border border-green-200
-
{{ end }} p-4"
-
>
-
{{ if gt (len .Comments) 0 }}
-
<div
-
class="absolute left-8 -top-4 w-px h-4 bg-gray-300"
-
></div>
-
{{ else }}
-
<div
-
class="absolute left-8 -top-8 w-px h-8 bg-gray-300"
-
></div>
-
{{ end }}
-
-
-
<div
-
class="flex items-center gap-2 {{ if .MergeCheck.IsConflicted }}
-
text-red-500
-
{{ else }}
-
text-green-500
-
{{ end }}"
-
>
-
{{ if .MergeCheck.IsConflicted }}
-
<i data-lucide="alert-triangle" class="w-4 h-4"></i>
-
<span class="font-medium"
-
>merge conflicts detected</span
-
>
-
{{ else }}
-
<i data-lucide="check-circle" class="w-4 h-4"></i>
-
<span class="font-medium">ready to merge</span>
-
{{ end }}
-
</div>
-
-
{{ if .MergeCheck.IsConflicted }}
-
<div class="mt-2">
-
<ul class="text-sm space-y-1">
-
{{ range .MergeCheck.Conflicts }}
-
<li class="flex items-center">
-
<i
-
data-lucide="file-warning"
-
class="w-3 h-3 mr-1.5 text-red-500"
-
></i>
-
<span class="font-mono"
-
>{{ slice .Filename 0 (sub (len .Filename) 2) }}</span
-
>
-
</li>
-
{{ end }}
-
</ul>
-
</div>
-
<div class="mt-3 text-sm text-gray-700">
-
<p>
-
Please resolve these conflicts locally and update
-
the patch to continue with the merge.
-
</p>
-
</div>
-
{{ else }}
-
<div class="mt-2 text-sm text-gray-700">
-
<p>
-
No conflicts detected with the base branch. This
-
pull request can be merged safely.
-
</p>
-
</div>
-
{{ if and .LoggedInUser (eq .LoggedInUser.Did .RepoInfo.OwnerDid) }}
-
<div class="mt-4 flex items-center gap-2">
-
<form
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge"
-
hx-swap="none"
-
>
-
<input
-
type="hidden"
-
name="targetBranch"
-
value="{{ .Pull.TargetBranch }}"
-
/>
-
<input
-
type="hidden"
-
name="patch"
-
value="{{ .Pull.Patch }}"
-
/>
-
<button
-
type="submit"
-
class="btn flex items-center gap-2"
-
{{ if or .Pull.State.IsClosed .MergeCheck.IsConflicted }}
-
disabled
-
{{ end }}
-
>
-
<i
-
data-lucide="git-merge"
-
class="w-4 h-4 text-purple-500"
-
></i>
-
<span>merge</span>
-
</button>
-
</form>
-
-
{{ if or (eq .LoggedInUser.Did .Pull.OwnerDid) (eq .LoggedInUser.Did .RepoInfo.OwnerDid) }}
-
{{ $action := "close" }}
-
{{ $icon := "circle-x" }}
-
{{ $hoverColor := "red" }}
-
{{ if .Pull.State.IsClosed }}
-
{{ $action = "reopen" }}
-
{{ $icon = "circle-dot" }}
-
{{ $hoverColor = "green" }}
-
{{ end }}
-
<form
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/{{ $action }}"
-
hx-swap="none"
-
>
-
<button
-
type="submit"
-
class="btn flex items-center gap-2"
-
>
-
<i
-
data-lucide="{{ $icon }}"
-
class="w-4 h-4 text-{{ $hoverColor }}-400"
-
></i>
-
<span>{{ $action }}</span>
-
</button>
-
</form>
-
<div id="pull-merge-error" class="error"></div>
-
<div
-
id="pull-merge-success"
-
class="success"
-
></div>
-
{{ end }}
-
</div>
-
{{ end }}
-
{{ end }}
-
</div>
-
{{ end }}
</section>
-
{{ if .LoggedInUser }}
-
<form
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/comment"
-
class="mt-8"
-
hx-swap="none"
-
>
-
<textarea
-
name="body"
-
class="w-full p-2 rounded border border-gray-200"
-
placeholder="Add to the discussion..."
-
></textarea>
-
<button type="submit" class="btn mt-2">comment</button>
-
<div id="pull-comment"></div>
-
</form>
-
{{ end }}
-
{{ if and (or (eq .LoggedInUser.Did .Pull.OwnerDid) (eq .LoggedInUser.Did .RepoInfo.OwnerDid)) (not .MergeCheck) (not .Pull.State.IsMerged) }}
{{ $action := "close" }}
{{ $icon := "circle-x" }}
{{ $hoverColor := "red" }}
···
{{ $icon = "circle-dot" }}
{{ $hoverColor = "green" }}
{{ end }}
-
<form
-
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/{{ $action }}"
-
hx-swap="none"
-
class="mt-8"
-
>
-
<button type="submit" class="btn text-sm flex items-center gap-2">
-
<i
-
data-lucide="{{ $icon }}"
-
class="w-4 h-4 mr-2 text-{{ $hoverColor }}-400"
-
></i>
-
<span class="text-black">{{ $action }}</span>
-
</button>
-
</form>
{{ end }}
-
<div id="pull-close"></div>
<div id="pull-reopen"></div>
{{ end }}
···
></i>
<span class="text-white">{{ .Pull.State.String }}</span>
</div>
+
<span class="text-gray-500 text-sm">
opened by
+
{{ $owner := index $.DidHandleMap .Pull.OwnerDid }}
<a href="/{{ $owner }}" class="no-underline hover:underline"
>{{ $owner }}</a
>
···
id="patch"
name="patch"
class="font-mono w-full h-full p-4 rounded-b border border-gray-200 text-sm hidden"
+
>{{- .Pull.Patch -}}</textarea>
<div class="flex gap-2 justify-end mt-2">
<button
···
{{ end }}
{{ define "repoAfter" }}
+
{{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }}
+
{{ $isRepoCollaborator := .RepoInfo.Roles.IsCollaborator }}
+
<section id="comments" class="mt-8 space-y-4 relative">
+
{{ block "comments" . }} {{ end }}
+
{{ if .Pull.State.IsMerged }}
+
{{ block "alreadyMergedCard" . }} {{ end }}
+
{{ else if .MergeCheck }}
+
{{ if .MergeCheck.IsConflicted }}
+
{{ block "isConflictedCard" . }} {{ end }}
+
{{ else }}
+
{{ block "noConflictsCard" . }} {{ end }}
+
{{ end }}
+
{{ end }}
</section>
+
{{ block "newComment" . }} {{ end }}
+
{{ if and (or $isPullAuthor $isRepoCollaborator) (not .Pull.State.IsMerged) }}
{{ $action := "close" }}
{{ $icon := "circle-x" }}
{{ $hoverColor := "red" }}
···
{{ $icon = "circle-dot" }}
{{ $hoverColor = "green" }}
{{ end }}
+
<button
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/{{ $action }}"
+
hx-swap="none"
+
class="btn mt-8 text-sm flex items-center gap-2">
+
<i data-lucide="{{ $icon }}" class="w-4 h-4 mr-2 text-{{ $hoverColor }}-400"></i>
+
<span class="text-black">{{ $action }}</span>
+
</button>
{{ end }}
<div id="pull-close"></div>
<div id="pull-reopen"></div>
{{ end }}
+
+
{{ define "comments" }}
+
{{ range $index, $comment := .Comments }}
+
<div
+
id="comment-{{ .CommentId }}"
+
class="rounded bg-white p-4 relative drop-shadow-sm"
+
>
+
{{ if eq $index 0 }}
+
<div
+
class="absolute left-8 -top-8 w-px h-8 bg-gray-300"
+
></div>
+
{{ else }}
+
<div
+
class="absolute left-8 -top-4 w-px h-4 bg-gray-300"
+
></div>
+
{{ end }}
+
<div class="flex items-center gap-2 mb-2 text-gray-400">
+
{{ $owner := index $.DidHandleMap .OwnerDid }}
+
<span class="text-sm">
+
<a
+
href="/{{ $owner }}"
+
class="no-underline hover:underline"
+
>{{ $owner }}</a
+
>
+
</span>
+
<span
+
class="px-1 select-none before:content-['\00B7']"
+
></span>
+
<a
+
href="#{{ .CommentId }}"
+
class="text-gray-500 text-sm hover:text-gray-500 hover:underline no-underline"
+
id="{{ .CommentId }}"
+
>
+
{{ .Created | timeFmt }}
+
</a>
+
</div>
+
<div class="prose">
+
{{ .Body | markdown }}
+
</div>
+
</div>
+
{{ end }}
+
{{ end }}
+
+
{{ define "newComment" }}
+
{{ if .LoggedInUser }}
+
<form
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/comment"
+
class="mt-8"
+
hx-swap="none">
+
<textarea
+
name="body"
+
class="w-full p-2 rounded border border-gray-200"
+
placeholder="Add to the discussion..."
+
></textarea>
+
<button type="submit" class="btn mt-2">comment</button>
+
<div id="pull-comment"></div>
+
</form>
+
{{ else }}
+
<div class="bg-white rounded drop-shadow-sm px-6 py-4 mt-8">
+
<a href="/login" class="underline">login</a> to join the discussion
+
</div>
+
{{ end }}
+
{{ end }}
+
+
{{ define "alreadyMergedCard" }}
+
<div
+
id="merge-status-card"
+
class="rounded relative bg-purple-50 border border-purple-200 p-4">
+
{{ if gt (len .Comments) 0 }}
+
<div
+
class="absolute left-8 -top-4 w-px h-4 bg-gray-300"
+
></div>
+
{{ else }}
+
<div
+
class="absolute left-8 -top-8 w-px h-8 bg-gray-300"
+
></div>
+
{{ end }}
+
+
+
<div class="flex items-center gap-2 text-purple-500">
+
<i data-lucide="git-merge" class="w-4 h-4"></i>
+
<span class="font-medium"
+
>Pull request successfully merged</span
+
>
+
</div>
+
+
<div class="mt-2 text-sm text-gray-700">
+
<p>This pull request has been merged into the base branch.</p>
+
</div>
+
</div>
+
{{ end }}
+
+
{{ define "isConflictedCard" }}
+
<div
+
id="merge-status-card"
+
class="rounded relative border bg-red-50 border-red-200 p-4">
+
{{ if gt (len .Comments) 0 }}
+
<div class="absolute left-8 -top-4 w-px h-4 bg-gray-300"></div>
+
{{ else }}
+
<div class="absolute left-8 -top-8 w-px h-8 bg-gray-300"></div>
+
{{ end }}
+
+
<div class="flex items-center gap-2 text-red-500">
+
<i data-lucide="alert-triangle" class="w-4 h-4"></i>
+
<span class="font-medium">merge conflicts detected</span>
+
</div>
+
+
<div class="mt-2">
+
<ul class="text-sm space-y-1">
+
{{ range .MergeCheck.Conflicts }}
+
<li class="flex items-center">
+
<i
+
data-lucide="file-warning"
+
class="w-3 h-3 mr-1.5 text-red-500"
+
></i>
+
<span class="font-mono"
+
>{{ slice .Filename 0 (sub (len .Filename) 2) }}</span
+
>
+
</li>
+
{{ end }}
+
</ul>
+
</div>
+
<div class="mt-3 text-sm text-gray-700">
+
<p>
+
Please resolve these conflicts locally and update
+
the patch to continue with the merge.
+
</p>
+
</div>
+
</div>
+
{{ end }}
+
+
+
{{ define "noConflictsCard" }}
+
{{ $isRepoCollaborator := .RepoInfo.Roles.IsCollaborator }}
+
<div
+
id="merge-status-card"
+
class="rounded relative border bg-green-50 border-green-200 p-4">
+
{{ if gt (len .Comments) 0 }}
+
<div class="absolute left-8 -top-4 w-px h-4 bg-gray-300"></div>
+
{{ else }}
+
<div class="absolute left-8 -top-8 w-px h-8 bg-gray-300"></div>
+
{{ end }}
+
+
<div class="flex items-center gap-2 text-green-500">
+
<i data-lucide="check-circle" class="w-4 h-4"></i>
+
<span class="font-medium">ready to merge</span>
+
</div>
+
+
<div class="mt-2 text-sm text-gray-700">
+
No conflicts detected with the base branch. This
+
pull request can be merged safely.
+
</div>
+
+
<div class="mt-4 flex items-center gap-2">
+
{{ if $isRepoCollaborator }}
+
<button
+
class="btn flex items-center gap-2"
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/merge"
+
hx-swap="none"
+
{{ if or .Pull.State.IsClosed .MergeCheck.IsConflicted }}
+
disabled
+
{{ end }}>
+
<i data-lucide="git-merge" class="w-4 h-4 text-purple-500"></i>
+
<span>merge</span>
+
</button>
+
{{ end }}
+
+
<div id="pull-merge-error" class="error"></div>
+
<div id="pull-merge-success" class="success"></div>
+
</div>
+
</div>
+
{{ end }}
+40 -2
appview/state/middleware.go
···
"context"
"log"
"net/http"
"strings"
"time"
···
}
}
-
func RoleMiddleware(s *State, group string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// requires auth also
···
}
}
func RepoPermissionMiddleware(s *State, requiredPerm string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
···
}
}
-
func ResolveRepoKnot(s *State) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
repoName := chi.URLParam(req, "repo")
···
})
}
}
···
"context"
"log"
"net/http"
+
"strconv"
"strings"
"time"
···
}
}
+
func knotRoleMiddleware(s *State, group string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// requires auth also
···
}
}
+
func KnotOwner(s *State) Middleware {
+
return knotRoleMiddleware(s, "server:owner")
+
}
+
func RepoPermissionMiddleware(s *State, requiredPerm string) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
···
}
}
+
func ResolveRepo(s *State) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
repoName := chi.URLParam(req, "repo")
···
})
}
}
+
+
// middleware that is tacked on top of /{user}/{repo}/pulls/{pull}
+
func ResolvePull(s *State) Middleware {
+
return func(next http.Handler) http.Handler {
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+
f, err := fullyResolvedRepo(r)
+
if err != nil {
+
log.Println("failed to fully resolve repo", err)
+
http.Error(w, "invalid repo url", http.StatusNotFound)
+
return
+
}
+
+
prId := chi.URLParam(r, "pull")
+
prIdInt, err := strconv.Atoi(prId)
+
if err != nil {
+
http.Error(w, "bad pr id", http.StatusBadRequest)
+
log.Println("failed to parse pr id", err)
+
return
+
}
+
+
pr, comments, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
+
if err != nil {
+
log.Println("failed to get pull and comments", err)
+
return
+
}
+
+
ctx := context.WithValue(r.Context(), "pull", pr)
+
ctx = context.WithValue(ctx, "pull_comments", comments)
+
+
next.ServeHTTP(w, r.WithContext(ctx))
+
})
+
}
+
}
+80 -74
appview/state/repo.go
···
func (s *State) EditPatch(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
-
f, err := fullyResolvedRepo(r)
-
if err != nil {
-
log.Println("failed to get repo and knot", err)
-
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
-
prId := chi.URLParam(r, "pull")
-
prIdInt, err := strconv.Atoi(prId)
-
if err != nil {
-
http.Error(w, "bad pr id", http.StatusBadRequest)
-
log.Println("failed to parse pr id", err)
return
}
-
patch := r.FormValue("patch")
-
if patch == "" {
-
s.pages.Notice(w, "pull-error", "Patch is required.")
return
}
-
// Get pull information before updating to get the atproto record URI
-
pull, _, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
if err != nil {
-
log.Println("failed to get pull information", err)
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
···
defer tx.Rollback()
// Update patch in the database within transaction
-
err = db.EditPatch(tx, f.RepoAt, prIdInt, patch)
if err != nil {
log.Println("failed to update patch", err)
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
···
return
}
-
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, prIdInt))
return
}
···
return
}
-
prId := chi.URLParam(r, "pull")
-
prIdInt, err := strconv.Atoi(prId)
-
if err != nil {
-
http.Error(w, "bad pr id", http.StatusBadRequest)
-
log.Println("failed to parse pr id", err)
-
return
-
}
-
-
pr, comments, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
-
if err != nil {
-
log.Println("failed to get pr and comments", err)
-
s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.")
return
}
-
pullOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), pr.OwnerDid)
-
if err != nil {
-
log.Println("failed to resolve pull owner", err)
-
}
-
identsToResolve := make([]string, len(comments))
for i, comment := range comments {
identsToResolve[i] = comment.OwnerDid
}
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
···
var mergeCheckResponse types.MergeCheckResponse
// Only perform merge check if the pull request is not already merged
-
if pr.State != db.PullMerged {
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("failed to get registration key for %s", f.Knot)
···
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err == nil {
-
resp, err := ksClient.MergeCheck([]byte(pr.Patch), pr.OwnerDid, f.RepoName, pr.TargetBranch)
if err != nil {
log.Println("failed to check for mergeability:", err)
} else {
···
}
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
-
Pull: *pr,
-
Comments: comments,
-
PullOwnerHandle: pullOwnerIdent.Handle.String(),
-
DidHandleMap: didHandleMap,
-
MergeCheck: mergeCheckResponse,
})
}
···
Description: f.Description,
IsStarred: isStarred,
Knot: knot,
-
Roles: rolesInRepo(s, u, f),
Stats: db.RepoStats{
StarCount: starCount,
IssueCount: issueCount,
···
return
}
-
// Get the pull request ID from the request URL
-
pullId := chi.URLParam(r, "pull")
-
pullIdInt, err := strconv.Atoi(pullId)
-
if err != nil {
-
log.Println("failed to parse pull ID:", err)
-
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
-
// Get the patch data from the request body
-
patch := r.FormValue("patch")
-
branch := r.FormValue("targetBranch")
-
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no registration key found for domain %s: %s\n", f.Knot, err)
···
}
// Merge the pull request
-
resp, err := ksClient.Merge([]byte(patch), user.Did, f.RepoName, branch)
if err != nil {
log.Printf("failed to merge pull request: %s", err)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
}
if resp.StatusCode == http.StatusOK {
-
err := db.MergePull(s.db, f.RepoAt, pullIdInt)
if err != nil {
log.Printf("failed to update pull request status in database: %s", err)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
-
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pullIdInt))
} else {
log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
}
func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {
f, err := fullyResolvedRepo(r)
if err != nil {
log.Println("malformed middleware")
return
}
-
pullId := chi.URLParam(r, "pull")
-
pullIdInt, err := strconv.Atoi(pullId)
-
if err != nil {
-
log.Println("malformed middleware")
return
}
···
}
// Close the pull in the database
-
err = db.ClosePull(tx, f.RepoAt, pullIdInt)
if err != nil {
log.Println("failed to close pull", err)
s.pages.Notice(w, "pull-close", "Failed to close pull.")
···
return
}
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullIdInt))
return
}
func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {
f, err := fullyResolvedRepo(r)
if err != nil {
log.Println("failed to resolve repo", err)
···
return
}
-
// Start a transaction
-
tx, err := s.db.BeginTx(r.Context(), nil)
-
if err != nil {
-
log.Println("failed to start transaction", err)
-
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
}
-
pullId := chi.URLParam(r, "pull")
-
pullIdInt, err := strconv.Atoi(pullId)
if err != nil {
-
log.Println("failed to parse pull id", err)
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
}
// Reopen the pull in the database
-
err = db.ReopenPull(tx, f.RepoAt, pullIdInt)
if err != nil {
log.Println("failed to reopen pull", err)
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
···
return
}
-
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullIdInt))
return
}
···
}, nil
}
-
func rolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo {
if u != nil {
r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo())
return pages.RolesInRepo{r}
···
func (s *State) EditPatch(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
+
+
patch := r.FormValue("patch")
+
if patch == "" {
+
s.pages.Notice(w, "pull-error", "Patch is required.")
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
+
if !ok {
+
log.Println("failed to get pull")
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
+
if pull.OwnerDid != user.Did {
+
log.Println("failed to edit pull information")
+
s.pages.Notice(w, "pull-error", "Unauthorized")
return
}
+
f, err := fullyResolvedRepo(r)
if err != nil {
+
log.Println("failed to get repo and knot", err)
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
···
defer tx.Rollback()
// Update patch in the database within transaction
+
err = db.EditPatch(tx, f.RepoAt, pull.PullId, patch)
if err != nil {
log.Println("failed to update patch", err)
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
···
return
}
+
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId))
return
}
···
return
}
+
pull, ok1 := r.Context().Value("pull").(*db.Pull)
+
comments, ok2 := r.Context().Value("pull_comments").([]db.PullComment)
+
if !ok1 || !ok2 {
+
log.Println("failed to get pull")
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
identsToResolve := make([]string, len(comments))
for i, comment := range comments {
identsToResolve[i] = comment.OwnerDid
}
+
identsToResolve = append(identsToResolve, pull.OwnerDid)
+
resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
didHandleMap := make(map[string]string)
for _, identity := range resolvedIds {
···
var mergeCheckResponse types.MergeCheckResponse
// Only perform merge check if the pull request is not already merged
+
if pull.State != db.PullMerged {
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("failed to get registration key for %s", f.Knot)
···
ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
if err == nil {
+
resp, err := ksClient.MergeCheck([]byte(pull.Patch), pull.OwnerDid, f.RepoName, pull.TargetBranch)
if err != nil {
log.Println("failed to check for mergeability:", err)
} else {
···
}
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
+
Pull: *pull,
+
Comments: comments,
+
DidHandleMap: didHandleMap,
+
MergeCheck: mergeCheckResponse,
})
}
···
Description: f.Description,
IsStarred: isStarred,
Knot: knot,
+
Roles: RolesInRepo(s, u, f),
Stats: db.RepoStats{
StarCount: starCount,
IssueCount: issueCount,
···
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
+
if !ok {
+
log.Println("failed to get pull")
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
secret, err := db.GetRegistrationKey(s.db, f.Knot)
if err != nil {
log.Printf("no registration key found for domain %s: %s\n", f.Knot, err)
···
}
// Merge the pull request
+
resp, err := ksClient.Merge([]byte(pull.Patch), user.Did, f.RepoName, pull.TargetBranch)
if err != nil {
log.Printf("failed to merge pull request: %s", err)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
}
if resp.StatusCode == http.StatusOK {
+
err := db.MergePull(s.db, f.RepoAt, pull.PullId)
if err != nil {
log.Printf("failed to update pull request status in database: %s", err)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
return
}
+
s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, pull.PullId))
} else {
log.Printf("knotserver returned non-OK status code for merge: %d", resp.StatusCode)
s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.")
···
}
func (s *State) ClosePull(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
f, err := fullyResolvedRepo(r)
if err != nil {
log.Println("malformed middleware")
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
+
if !ok {
+
log.Println("failed to get pull")
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
+
return
+
}
+
+
// auth filter: only owner or collaborators can close
+
roles := RolesInRepo(s, user, f)
+
isCollaborator := roles.IsCollaborator()
+
isPullAuthor := user.Did == pull.OwnerDid
+
isCloseAllowed := isCollaborator || isPullAuthor
+
if !isCloseAllowed {
+
log.Println("failed to close pull")
+
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
return
}
···
}
// Close the pull in the database
+
err = db.ClosePull(tx, f.RepoAt, pull.PullId)
if err != nil {
log.Println("failed to close pull", err)
s.pages.Notice(w, "pull-close", "Failed to close pull.")
···
return
}
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
return
}
func (s *State) ReopenPull(w http.ResponseWriter, r *http.Request) {
+
user := s.auth.GetUser(r)
+
f, err := fullyResolvedRepo(r)
if err != nil {
log.Println("failed to resolve repo", err)
···
return
}
+
pull, ok := r.Context().Value("pull").(*db.Pull)
+
if !ok {
+
log.Println("failed to get pull")
+
s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.")
return
}
+
// auth filter: only owner or collaborators can close
+
roles := RolesInRepo(s, user, f)
+
isCollaborator := roles.IsCollaborator()
+
isPullAuthor := user.Did == pull.OwnerDid
+
isCloseAllowed := isCollaborator || isPullAuthor
+
if !isCloseAllowed {
+
log.Println("failed to close pull")
+
s.pages.Notice(w, "pull-close", "You are unauthorized to close this pull.")
+
return
+
}
+
+
// Start a transaction
+
tx, err := s.db.BeginTx(r.Context(), nil)
if err != nil {
+
log.Println("failed to start transaction", err)
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
return
}
// Reopen the pull in the database
+
err = db.ReopenPull(tx, f.RepoAt, pull.PullId)
if err != nil {
log.Println("failed to reopen pull", err)
s.pages.Notice(w, "pull-reopen", "Failed to reopen pull.")
···
return
}
+
s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId))
return
}
···
}, nil
}
+
func RolesInRepo(s *State, u *auth.User, f *FullyResolvedRepo) pages.RolesInRepo {
if u != nil {
r := s.enforcer.GetPermissionsInRepo(u.Did, f.Knot, f.OwnerSlashRepo())
return pages.RolesInRepo{r}
+24 -12
appview/state/router.go
···
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
r.Get("/", s.ProfilePage)
-
r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) {
r.Get("/", s.RepoIndex)
r.Get("/commits/{ref}", s.RepoLog)
r.Route("/tree/{ref}", func(r chi.Router) {
···
r.Route("/pulls", func(r chi.Router) {
r.Get("/", s.RepoPulls)
-
r.Get("/{pull}", s.RepoSinglePull)
-
r.Group(func(r chi.Router) {
-
r.Use(AuthMiddleware(s))
-
r.Get("/new", s.NewPull)
-
r.Post("/new", s.NewPull)
-
r.Patch("/{pull}/patch", s.EditPatch)
-
r.Post("/{pull}/comment", s.PullComment)
-
r.Post("/{pull}/close", s.ClosePull)
-
r.Post("/{pull}/reopen", s.ReopenPull)
-
r.Post("/{pull}/merge", s.MergePull)
})
})
···
r.Post("/init", s.InitKnotServer)
r.Get("/", s.KnotServerInfo)
r.Route("/member", func(r chi.Router) {
-
r.Use(RoleMiddleware(s, "server:owner"))
r.Get("/", s.ListMembers)
r.Put("/", s.AddMember)
r.Delete("/", s.RemoveMember)
···
r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) {
r.Get("/", s.ProfilePage)
+
r.With(ResolveRepo(s)).Route("/{repo}", func(r chi.Router) {
r.Get("/", s.RepoIndex)
r.Get("/commits/{ref}", s.RepoLog)
r.Route("/tree/{ref}", func(r chi.Router) {
···
r.Route("/pulls", func(r chi.Router) {
r.Get("/", s.RepoPulls)
+
r.With(AuthMiddleware(s)).Route("/new", func(r chi.Router) {
+
r.Get("/", s.NewPull)
+
r.Post("/", s.NewPull)
+
})
+
r.Route("/{pull}", func(r chi.Router) {
+
r.Use(ResolvePull(s))
+
r.Get("/", s.RepoSinglePull)
+
+
// authorized requests below this point
+
r.Group(func(r chi.Router) {
+
r.Use(AuthMiddleware(s))
+
r.Patch("/patch", s.EditPatch)
+
r.Post("/comment", s.PullComment)
+
r.Post("/close", s.ClosePull)
+
r.Post("/reopen", s.ReopenPull)
+
// collaborators only
+
r.Group(func(r chi.Router) {
+
r.Use(RepoPermissionMiddleware(s, "repo:collaborator"))
+
r.Post("/merge", s.MergePull)
+
// maybe lock, etc.
+
})
+
})
})
})
···
r.Post("/init", s.InitKnotServer)
r.Get("/", s.KnotServerInfo)
r.Route("/member", func(r chi.Router) {
+
r.Use(KnotOwner(s))
r.Get("/", s.ListMembers)
r.Put("/", s.AddMember)
r.Delete("/", s.RemoveMember)
+1 -1
flake.nix
···
inherit (gitignore.lib) gitignoreSource;
in {
overlays.default = final: prev: let
-
goModHash = "sha256-k+WeNx9jZ5YGgskCJYiU2mwyz25E0bhFgSg2GDWZXFw=";
buildCmdPackage = name:
final.buildGoModule {
pname = name;
···
inherit (gitignore.lib) gitignoreSource;
in {
overlays.default = final: prev: let
+
goModHash = "sha256-zJKjcxd+gr+9Kx2e1lUv+0hlXlxJm5YbWeIGUo0eIiE=";
buildCmdPackage = name:
final.buildGoModule {
pname = name;