diff --git a/appview/db/pulls.go b/appview/db/pulls.go index bf9db112..793b261f 100644 --- a/appview/db/pulls.go +++ b/appview/db/pulls.go @@ -233,6 +233,11 @@ func ReopenPull(e Execer, repoAt syntax.ATURI, pullId int) error { 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 diff --git a/appview/pages/pages.go b/appview/pages/pages.go index d0eb8d81..a8cfa8ec 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -531,15 +531,26 @@ func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error { } type RepoSinglePullParams struct { - LoggedInUser *auth.User - RepoInfo RepoInfo - DidHandleMap map[string]string - Pull db.Pull - Comments []db.PullComment - Active string + 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) } diff --git a/appview/pages/templates/repo/pulls/new.html b/appview/pages/templates/repo/pulls/new.html index f7884cc1..421832f9 100644 --- a/appview/pages/templates/repo/pulls/new.html +++ b/appview/pages/templates/repo/pulls/new.html @@ -10,6 +10,11 @@
+ + +

+ The branch you want to make your change against. +

@@ -21,13 +26,17 @@ class="w-full resize-y" placeholder="Describe your change. Markdown is supported." > - + +
+ + +
diff --git a/appview/pages/templates/repo/pulls/pull.html b/appview/pages/templates/repo/pulls/pull.html index bd30054d..3ea4d869 100644 --- a/appview/pages/templates/repo/pulls/pull.html +++ b/appview/pages/templates/repo/pulls/pull.html @@ -1,5 +1,5 @@ {{ define "title" }} - {{ .Pull.Title }} · + {{ .Pull.Title }} · pull #{{ .Pull.PullId }} · {{ .RepoInfo.FullName }} {{ end }} @@ -8,12 +8,14 @@ {{ .Pull.Title }} #{{ .Pull.PullId }} - {{ $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 }} @@ -46,6 +48,49 @@ {{ end }} + +
+
+ + + patch + +
+       {{- .Pull.Patch -}}
+        
+
+
+ +
+ {{ if .MergeCheck }} +
+
+ {{ if .MergeCheck.IsConflicted }} + + merge conflicts detected + {{ else }} + + ready to merge + {{ end }} +
+ + {{ if .MergeCheck.IsConflicted }} +
+
    + {{ range .MergeCheck.Conflicts }} +
  • + + {{ slice .Filename 0 (sub (len .Filename) 2) }} +
  • + {{ end }} +
+
+ {{ end }} +
+ {{ end }} +
{{ end }} {{ define "repoAfter" }} diff --git a/appview/pages/templates/settings.html b/appview/pages/templates/settings.html index b580117f..c8e844e2 100644 --- a/appview/pages/templates/settings.html +++ b/appview/pages/templates/settings.html @@ -12,7 +12,8 @@ {{ end }} {{ define "profile" }} -
profile
+<<<<<<< HEAD +

profile

{{ if .LoggedInUser.Handle }} @@ -28,7 +29,8 @@ {{ end }} {{ define "keys" }} -
ssh keys
+<<<<<<< HEAD +

ssh keys

{{ range .PubKeys }} diff --git a/appview/state/repo.go b/appview/state/repo.go index 57c29e92..26c6879c 100644 --- a/appview/state/repo.go +++ b/appview/state/repo.go @@ -404,6 +404,11 @@ func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { 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 @@ -418,13 +423,42 @@ func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) { } } - s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ - LoggedInUser: user, - RepoInfo: f.RepoInfo(s, user), - Pull: *pr, - Comments: comments, + 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 + } - DidHandleMap: didHandleMap, + 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, }) } diff --git a/appview/state/router.go b/appview/state/router.go new file mode 100644 index 00000000..20b9c511 --- /dev/null +++ b/appview/state/router.go @@ -0,0 +1,164 @@ +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 +} diff --git a/appview/state/state.go b/appview/state/state.go index 7c0b4fb5..32dc2d0c 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -829,160 +829,3 @@ func GetAvatarUri(did string) (string, error) { 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 -} diff --git a/input.css b/input.css index 0c0ee11a..9c21b476 100644 --- a/input.css +++ b/input.css @@ -110,20 +110,20 @@ font-size: 14px; } a { - @apply no-underline text-black hover:underline hover:text-gray-800; + @apply no-underline text-black hover:underline hover:text-gray-800; } label { - @apply block text-sm text-black; + @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-2; + @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-2; + @apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3; } details summary::-webkit-details-marker { - display: none; + display: none; } } diff --git a/knotserver/routes.go b/knotserver/routes.go index 429163da..67e3e796 100644 --- a/knotserver/routes.go +++ b/knotserver/routes.go @@ -628,7 +628,10 @@ func (h *Handle) MergeCheck(w http.ResponseWriter, r *http.Request) { err = gr.MergeCheck([]byte(patch), branch) if err == nil { - w.WriteHeader(http.StatusOK) + response := types.MergeCheckResponse{ + IsConflicted: false, + } + writeJSON(w, response) return }