forked from tangled.org/core
this repo has no description

appview: render single pull

Changed files
+302 -186
appview
knotserver
+5
appview/db/pulls.go
···
return err
}
type PullCount struct {
Open int
Closed int
···
return err
}
+
func MergePull(e Execer, repoAt syntax.ATURI, pullId int) error {
+
_, err := e.Exec(`update pulls set open = 2 where repo_at = ? and pull_id = ?`, repoAt, pullId)
+
return err
+
}
+
type PullCount struct {
Open int
Closed int
+17 -6
appview/pages/pages.go
···
}
type RepoSinglePullParams struct {
-
LoggedInUser *auth.User
-
RepoInfo RepoInfo
-
DidHandleMap map[string]string
-
Pull db.Pull
-
Comments []db.PullComment
-
Active string
}
func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
params.Active = "pulls"
return p.executeRepo("repo/pulls/pull", w, params)
}
···
}
type RepoSinglePullParams struct {
+
LoggedInUser *auth.User
+
RepoInfo RepoInfo
+
DidHandleMap map[string]string
+
Pull db.Pull
+
State string
+
PullOwnerHandle string
+
Comments []db.PullComment
+
Active string
+
MergeCheck types.MergeCheckResponse
}
func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
+
switch params.Pull.Open {
+
case 0:
+
params.State = "close"
+
case 1:
+
params.State = "open"
+
case 2:
+
params.State = "merged"
+
}
params.Active = "pulls"
return p.executeRepo("repo/pulls/pull", w, params)
}
+16 -7
appview/pages/templates/repo/pulls/new.html
···
<div>
<label for="title">title</label>
<input type="text" name="title" id="title" class="w-full" />
<input type="text" name="targetBranch" id="targetBranch" />
</div>
<div>
···
class="w-full resize-y"
placeholder="Describe your change. Markdown is supported."
></textarea>
-
<textarea
-
name="patch"
-
id="patch"
-
rows="10"
-
class="w-full resize-y font-mono"
-
placeholder="Paste your git-format-patch output here."
-
></textarea>
</div>
<div>
<button type="submit" class="btn">create</button>
···
<div>
<label for="title">title</label>
<input type="text" name="title" id="title" class="w-full" />
+
+
<label for="targetBranch">target branch</label>
+
<p class="text-gray-500">
+
The branch you want to make your change against.
+
</p>
<input type="text" name="targetBranch" id="targetBranch" />
</div>
<div>
···
class="w-full resize-y"
placeholder="Describe your change. Markdown is supported."
></textarea>
+
+
<div class="mt-4">
+
<label for="patch">paste your patch here</label>
+
<textarea
+
name="patch"
+
id="patch"
+
rows="10"
+
class="w-full resize-y font-mono"
+
placeholder="Paste your git-format-patch output here."
+
></textarea>
+
</div>
</div>
<div>
<button type="submit" class="btn">create</button>
+47 -2
appview/pages/templates/repo/pulls/pull.html
···
{{ define "title" }}
-
{{ .Pull.Title }} &middot;
{{ .RepoInfo.FullName }}
{{ end }}
···
{{ .Pull.Title }}
<span class="text-gray-400">#{{ .Pull.PullId }}</span>
</h1>
-
{{ $bgColor := "bg-gray-800" }}
{{ $icon := "ban" }}
{{ if eq .State "open" }}
{{ $bgColor = "bg-green-600" }}
{{ $icon = "circle-dot" }}
{{ end }}
···
</article>
{{ end }}
</section>
{{ end }}
{{ define "repoAfter" }}
···
{{ define "title" }}
+
{{ .Pull.Title }} &middot; pull #{{ .Pull.PullId }} &middot;
{{ .RepoInfo.FullName }}
{{ end }}
···
{{ .Pull.Title }}
<span class="text-gray-400">#{{ .Pull.PullId }}</span>
</h1>
{{ $bgColor := "bg-gray-800" }}
{{ $icon := "ban" }}
{{ if eq .State "open" }}
{{ $bgColor = "bg-green-600" }}
{{ $icon = "circle-dot" }}
+
{{ else if eq .State "merged" }}
+
{{ $bgColor = "bg-purple-600" }}
+
{{ $icon = "git-merge" }}
{{ end }}
···
</article>
{{ end }}
</section>
+
+
<div>
+
<details>
+
<summary
+
class="list-none cursor-pointer sticky top-0 bg-white rounded-sm px-3 py-2 border border-gray-200 flex items-center text-gray-700 hover:bg-gray-50 transition-colors"
+
>
+
<i data-lucide="code" class="w-4 h-4 mr-2"></i>
+
<span>patch</span>
+
</summary>
+
<pre class="font-mono overflow-x-scroll bg-gray-50 p-4 rounded-b border border-gray-200 text-sm">
+
{{- .Pull.Patch -}}
+
</pre>
+
</details>
+
</div>
+
+
<div class="mt-4">
+
{{ if .MergeCheck }}
+
<div class="rounded-sm border p-4 {{ if .MergeCheck.IsConflicted }}bg-red-50 border-red-200{{ else }}bg-green-50 border-green-200{{ end }}">
+
<div class="flex items-center gap-2 rounded-sm {{ 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>
+
{{ end }}
+
</div>
+
{{ end }}
+
</div>
{{ end }}
{{ define "repoAfter" }}
+4 -2
appview/pages/templates/settings.html
···
{{ end }}
{{ define "profile" }}
-
<header class="text-sm font-bold py-2 px-6 uppercase">profile</header>
<section class="rounded bg-white drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
<dl class="grid grid-cols-[auto_1fr] gap-x-4">
{{ if .LoggedInUser.Handle }}
···
{{ end }}
{{ define "keys" }}
-
<header class="text-sm font-bold py-2 px-6 uppercase">ssh keys</header>
<section class="rounded bg-white drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
<div id="key-list" class="flex flex-col gap-6 mb-8">
{{ range .PubKeys }}
···
{{ end }}
{{ define "profile" }}
+
<<<<<<< HEAD
+
<h2 class="text-sm font-bold py-2 px-6 uppercase">profile</h2>
<section class="rounded bg-white drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
<dl class="grid grid-cols-[auto_1fr] gap-x-4">
{{ if .LoggedInUser.Handle }}
···
{{ end }}
{{ define "keys" }}
+
<<<<<<< HEAD
+
<h2 class="text-sm font-bold py-2 px-6 uppercase">ssh keys</h2>
<section class="rounded bg-white drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit">
<div id="key-list" class="flex flex-col gap-6 mb-8">
{{ range .PubKeys }}
+40 -6
appview/state/repo.go
···
return
}
identsToResolve := make([]string, len(comments))
for i, comment := range comments {
identsToResolve[i] = comment.OwnerDid
···
}
}
-
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
-
Pull: *pr,
-
Comments: comments,
-
DidHandleMap: didHandleMap,
})
}
···
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
···
}
}
+
secret, err := db.GetRegistrationKey(s.db, f.Knot)
+
if err != nil {
+
log.Printf("failed to get registration key for %s", f.Knot)
+
s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.")
+
return
+
}
+
var mergeCheckResponse types.MergeCheckResponse
+
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 {
+
respBody, err := io.ReadAll(resp.Body)
+
if err != nil {
+
log.Println("failed to read merge check response body")
+
} else {
+
err = json.Unmarshal(respBody, &mergeCheckResponse)
+
if err != nil {
+
log.Println("failed to unmarshal merge check response", err)
+
}
+
}
+
}
+
} else {
+
log.Printf("failed to setup signed client for %s; ignoring...", f.Knot)
+
}
+
+
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,
})
}
+164
appview/state/router.go
···
···
+
package state
+
+
import (
+
"net/http"
+
"strings"
+
+
"github.com/go-chi/chi/v5"
+
)
+
+
func (s *State) Router() http.Handler {
+
router := chi.NewRouter()
+
+
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
+
pat := chi.URLParam(r, "*")
+
if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
+
s.UserRouter().ServeHTTP(w, r)
+
} else {
+
s.StandardRouter().ServeHTTP(w, r)
+
}
+
})
+
+
return router
+
}
+
+
func (s *State) UserRouter() http.Handler {
+
r := chi.NewRouter()
+
+
// strip @ from user
+
r.Use(StripLeadingAt)
+
+
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.Get("/", s.RepoIndex)
+
r.Get("/*", s.RepoTree)
+
})
+
r.Get("/commit/{ref}", s.RepoCommit)
+
r.Get("/branches", s.RepoBranches)
+
r.Get("/tags", s.RepoTags)
+
r.Get("/blob/{ref}/*", s.RepoBlob)
+
+
r.Route("/issues", func(r chi.Router) {
+
r.Get("/", s.RepoIssues)
+
r.Get("/{issue}", s.RepoSingleIssue)
+
+
r.Group(func(r chi.Router) {
+
r.Use(AuthMiddleware(s))
+
r.Get("/new", s.NewIssue)
+
r.Post("/new", s.NewIssue)
+
r.Post("/{issue}/comment", s.IssueComment)
+
r.Post("/{issue}/close", s.CloseIssue)
+
r.Post("/{issue}/reopen", s.ReopenIssue)
+
})
+
})
+
+
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.Post("/{pull}/comment", s.PullComment)
+
// r.Post("/{pull}/close", s.ClosePull)
+
// r.Post("/{pull}/reopen", s.ReopenPull)
+
// r.Post("/{pull}/merge", s.MergePull)
+
})
+
})
+
+
// These routes get proxied to the knot
+
r.Get("/info/refs", s.InfoRefs)
+
r.Post("/git-upload-pack", s.UploadPack)
+
+
// settings routes, needs auth
+
r.Group(func(r chi.Router) {
+
r.Use(AuthMiddleware(s))
+
// repo description can only be edited by owner
+
r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) {
+
r.Put("/", s.RepoDescription)
+
r.Get("/", s.RepoDescription)
+
r.Get("/edit", s.RepoDescriptionEdit)
+
})
+
r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
+
r.Get("/", s.RepoSettings)
+
r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
+
})
+
})
+
})
+
})
+
+
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
+
s.pages.Error404(w)
+
})
+
+
return r
+
}
+
+
func (s *State) StandardRouter() http.Handler {
+
r := chi.NewRouter()
+
+
r.Handle("/static/*", s.pages.Static())
+
+
r.Get("/", s.Timeline)
+
+
r.With(AuthMiddleware(s)).Get("/logout", s.Logout)
+
+
r.Route("/login", func(r chi.Router) {
+
r.Get("/", s.Login)
+
r.Post("/", s.Login)
+
})
+
+
r.Route("/knots", func(r chi.Router) {
+
r.Use(AuthMiddleware(s))
+
r.Get("/", s.Knots)
+
r.Post("/key", s.RegistrationKey)
+
+
r.Route("/{domain}", func(r chi.Router) {
+
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.Route("/repo", func(r chi.Router) {
+
r.Route("/new", func(r chi.Router) {
+
r.Use(AuthMiddleware(s))
+
r.Get("/", s.NewRepo)
+
r.Post("/", s.NewRepo)
+
})
+
// r.Post("/import", s.ImportRepo)
+
})
+
+
r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
+
r.Post("/", s.Follow)
+
r.Delete("/", s.Follow)
+
})
+
+
r.With(AuthMiddleware(s)).Route("/star", func(r chi.Router) {
+
r.Post("/", s.Star)
+
r.Delete("/", s.Star)
+
})
+
+
r.Route("/settings", func(r chi.Router) {
+
r.Use(AuthMiddleware(s))
+
r.Get("/", s.Settings)
+
r.Put("/keys", s.SettingsKeys)
+
})
+
+
r.Get("/keys/{user}", s.Keys)
+
+
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
+
s.pages.Error404(w)
+
})
+
return r
+
}
-157
appview/state/state.go
···
return fmt.Sprintf("https://cdn.bsky.app/img/feed_thumbnail/plain/%s/%s", did, link), nil
}
-
-
func (s *State) Router() http.Handler {
-
router := chi.NewRouter()
-
-
router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) {
-
pat := chi.URLParam(r, "*")
-
if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") {
-
s.UserRouter().ServeHTTP(w, r)
-
} else {
-
s.StandardRouter().ServeHTTP(w, r)
-
}
-
})
-
-
return router
-
}
-
-
func (s *State) UserRouter() http.Handler {
-
r := chi.NewRouter()
-
-
// strip @ from user
-
r.Use(StripLeadingAt)
-
-
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.Get("/", s.RepoIndex)
-
r.Get("/*", s.RepoTree)
-
})
-
r.Get("/commit/{ref}", s.RepoCommit)
-
r.Get("/branches", s.RepoBranches)
-
r.Get("/tags", s.RepoTags)
-
r.Get("/blob/{ref}/*", s.RepoBlob)
-
-
r.Route("/issues", func(r chi.Router) {
-
r.Get("/", s.RepoIssues)
-
r.Get("/{issue}", s.RepoSingleIssue)
-
-
r.Group(func(r chi.Router) {
-
r.Use(AuthMiddleware(s))
-
r.Get("/new", s.NewIssue)
-
r.Post("/new", s.NewIssue)
-
r.Post("/{issue}/comment", s.IssueComment)
-
r.Post("/{issue}/close", s.CloseIssue)
-
r.Post("/{issue}/reopen", s.ReopenIssue)
-
})
-
})
-
-
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.Post("/{pull}/comment", s.PullComment)
-
// r.Post("/{pull}/close", s.ClosePull)
-
// r.Post("/{pull}/reopen", s.ReopenPull)
-
// r.Post("/{pull}/merge", s.MergePull)
-
})
-
})
-
-
// These routes get proxied to the knot
-
r.Get("/info/refs", s.InfoRefs)
-
r.Post("/git-upload-pack", s.UploadPack)
-
-
// settings routes, needs auth
-
r.Group(func(r chi.Router) {
-
r.Use(AuthMiddleware(s))
-
// repo description can only be edited by owner
-
r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) {
-
r.Put("/", s.RepoDescription)
-
r.Get("/", s.RepoDescription)
-
r.Get("/edit", s.RepoDescriptionEdit)
-
})
-
r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) {
-
r.Get("/", s.RepoSettings)
-
r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator)
-
})
-
})
-
})
-
})
-
-
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
-
s.pages.Error404(w)
-
})
-
-
return r
-
}
-
-
func (s *State) StandardRouter() http.Handler {
-
r := chi.NewRouter()
-
-
r.Handle("/static/*", s.pages.Static())
-
-
r.Get("/", s.Timeline)
-
-
r.With(AuthMiddleware(s)).Get("/logout", s.Logout)
-
-
r.Route("/login", func(r chi.Router) {
-
r.Get("/", s.Login)
-
r.Post("/", s.Login)
-
})
-
-
r.Route("/knots", func(r chi.Router) {
-
r.Use(AuthMiddleware(s))
-
r.Get("/", s.Knots)
-
r.Post("/key", s.RegistrationKey)
-
-
r.Route("/{domain}", func(r chi.Router) {
-
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.Route("/repo", func(r chi.Router) {
-
r.Route("/new", func(r chi.Router) {
-
r.Use(AuthMiddleware(s))
-
r.Get("/", s.NewRepo)
-
r.Post("/", s.NewRepo)
-
})
-
// r.Post("/import", s.ImportRepo)
-
})
-
-
r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {
-
r.Post("/", s.Follow)
-
r.Delete("/", s.Follow)
-
})
-
-
r.With(AuthMiddleware(s)).Route("/star", func(r chi.Router) {
-
r.Post("/", s.Star)
-
r.Delete("/", s.Star)
-
})
-
-
r.Route("/settings", func(r chi.Router) {
-
r.Use(AuthMiddleware(s))
-
r.Get("/", s.Settings)
-
r.Put("/keys", s.SettingsKeys)
-
r.Delete("/keys", s.SettingsKeys)
-
})
-
-
r.Get("/keys/{user}", s.Keys)
-
-
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
-
s.pages.Error404(w)
-
})
-
return r
-
}
···
return fmt.Sprintf("https://cdn.bsky.app/img/feed_thumbnail/plain/%s/%s", did, link), nil
}
+5 -5
input.css
···
font-size: 14px;
}
a {
-
@apply no-underline text-black hover:underline hover:text-gray-800;
}
label {
-
@apply block text-sm text-black;
}
input {
-
@apply bg-white border border-gray-400 rounded-sm focus:ring-black p-2;
}
textarea {
-
@apply bg-white border border-gray-400 rounded-sm focus:ring-black p-2;
}
details summary::-webkit-details-marker {
-
display: none;
}
}
···
font-size: 14px;
}
a {
+
@apply no-underline text-black hover:underline hover:text-gray-800;
}
label {
+
@apply block mb-2 text-gray-900 text-sm font-bold py-2 uppercase;
}
input {
+
@apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3;
}
textarea {
+
@apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3;
}
details summary::-webkit-details-marker {
+
display: none;
}
}
+4 -1
knotserver/routes.go
···
err = gr.MergeCheck([]byte(patch), branch)
if err == nil {
-
w.WriteHeader(http.StatusOK)
return
}
···
err = gr.MergeCheck([]byte(patch), branch)
if err == nil {
+
response := types.MergeCheckResponse{
+
IsConflicted: false,
+
}
+
writeJSON(w, response)
return
}