Monorepo for Tangled tangled.org

appview: render single pull

authored by anirudh.fi and committed by oppi.li 0d7fe809 7415d134

Changed files
+302 -186
appview
knotserver
+5
appview/db/pulls.go
··· 233 233 return err 234 234 } 235 235 236 + func MergePull(e Execer, repoAt syntax.ATURI, pullId int) error { 237 + _, err := e.Exec(`update pulls set open = 2 where repo_at = ? and pull_id = ?`, repoAt, pullId) 238 + return err 239 + } 240 + 236 241 type PullCount struct { 237 242 Open int 238 243 Closed int
+17 -6
appview/pages/pages.go
··· 531 531 } 532 532 533 533 type RepoSinglePullParams struct { 534 - LoggedInUser *auth.User 535 - RepoInfo RepoInfo 536 - DidHandleMap map[string]string 537 - Pull db.Pull 538 - Comments []db.PullComment 539 - Active string 534 + LoggedInUser *auth.User 535 + RepoInfo RepoInfo 536 + DidHandleMap map[string]string 537 + Pull db.Pull 538 + State string 539 + PullOwnerHandle string 540 + Comments []db.PullComment 541 + Active string 542 + MergeCheck types.MergeCheckResponse 540 543 } 541 544 542 545 func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { 546 + switch params.Pull.Open { 547 + case 0: 548 + params.State = "close" 549 + case 1: 550 + params.State = "open" 551 + case 2: 552 + params.State = "merged" 553 + } 543 554 params.Active = "pulls" 544 555 return p.executeRepo("repo/pulls/pull", w, params) 545 556 }
+16 -7
appview/pages/templates/repo/pulls/new.html
··· 10 10 <div> 11 11 <label for="title">title</label> 12 12 <input type="text" name="title" id="title" class="w-full" /> 13 + 14 + <label for="targetBranch">target branch</label> 15 + <p class="text-gray-500"> 16 + The branch you want to make your change against. 17 + </p> 13 18 <input type="text" name="targetBranch" id="targetBranch" /> 14 19 </div> 15 20 <div> ··· 21 26 class="w-full resize-y" 22 27 placeholder="Describe your change. Markdown is supported." 23 28 ></textarea> 24 - <textarea 25 - name="patch" 26 - id="patch" 27 - rows="10" 28 - class="w-full resize-y font-mono" 29 - placeholder="Paste your git-format-patch output here." 30 - ></textarea> 29 + 30 + <div class="mt-4"> 31 + <label for="patch">paste your patch here</label> 32 + <textarea 33 + name="patch" 34 + id="patch" 35 + rows="10" 36 + class="w-full resize-y font-mono" 37 + placeholder="Paste your git-format-patch output here." 38 + ></textarea> 39 + </div> 31 40 </div> 32 41 <div> 33 42 <button type="submit" class="btn">create</button>
+47 -2
appview/pages/templates/repo/pulls/pull.html
··· 1 1 {{ define "title" }} 2 - {{ .Pull.Title }} &middot; 2 + {{ .Pull.Title }} &middot; pull #{{ .Pull.PullId }} &middot; 3 3 {{ .RepoInfo.FullName }} 4 4 {{ end }} 5 5 ··· 8 8 {{ .Pull.Title }} 9 9 <span class="text-gray-400">#{{ .Pull.PullId }}</span> 10 10 </h1> 11 - 12 11 {{ $bgColor := "bg-gray-800" }} 13 12 {{ $icon := "ban" }} 14 13 {{ if eq .State "open" }} 15 14 {{ $bgColor = "bg-green-600" }} 16 15 {{ $icon = "circle-dot" }} 16 + {{ else if eq .State "merged" }} 17 + {{ $bgColor = "bg-purple-600" }} 18 + {{ $icon = "git-merge" }} 17 19 {{ end }} 18 20 19 21 ··· 46 48 </article> 47 49 {{ end }} 48 50 </section> 51 + 52 + <div> 53 + <details> 54 + <summary 55 + 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" 56 + > 57 + <i data-lucide="code" class="w-4 h-4 mr-2"></i> 58 + <span>patch</span> 59 + </summary> 60 + <pre class="font-mono overflow-x-scroll bg-gray-50 p-4 rounded-b border border-gray-200 text-sm"> 61 + {{- .Pull.Patch -}} 62 + </pre> 63 + </details> 64 + </div> 65 + 66 + <div class="mt-4"> 67 + {{ if .MergeCheck }} 68 + <div class="rounded-sm border p-4 {{ if .MergeCheck.IsConflicted }}bg-red-50 border-red-200{{ else }}bg-green-50 border-green-200{{ end }}"> 69 + <div class="flex items-center gap-2 rounded-sm {{ if .MergeCheck.IsConflicted }}text-red-500{{ else }}text-green-500 {{ end }}"> 70 + {{ if .MergeCheck.IsConflicted }} 71 + <i data-lucide="alert-triangle" class="w-4 h-4"></i> 72 + <span class="font-medium">merge conflicts detected</span> 73 + {{ else }} 74 + <i data-lucide="check-circle" class="w-4 h-4"></i> 75 + <span class="font-medium">ready to merge</span> 76 + {{ end }} 77 + </div> 78 + 79 + {{ if .MergeCheck.IsConflicted }} 80 + <div class="mt-2"> 81 + <ul class="text-sm space-y-1"> 82 + {{ range .MergeCheck.Conflicts }} 83 + <li class="flex items-center"> 84 + <i data-lucide="file-warning" class="w-3 h-3 mr-1.5 text-red-500"></i> 85 + <span class="font-mono">{{ slice .Filename 0 (sub (len .Filename) 2) }}</span> 86 + </li> 87 + {{ end }} 88 + </ul> 89 + </div> 90 + {{ end }} 91 + </div> 92 + {{ end }} 93 + </div> 49 94 {{ end }} 50 95 51 96 {{ define "repoAfter" }}
+4 -2
appview/pages/templates/settings.html
··· 12 12 {{ end }} 13 13 14 14 {{ define "profile" }} 15 - <header class="text-sm font-bold py-2 px-6 uppercase">profile</header> 15 + <<<<<<< HEAD 16 + <h2 class="text-sm font-bold py-2 px-6 uppercase">profile</h2> 16 17 <section class="rounded bg-white drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 17 18 <dl class="grid grid-cols-[auto_1fr] gap-x-4"> 18 19 {{ if .LoggedInUser.Handle }} ··· 28 29 {{ end }} 29 30 30 31 {{ define "keys" }} 31 - <header class="text-sm font-bold py-2 px-6 uppercase">ssh keys</header> 32 + <<<<<<< HEAD 33 + <h2 class="text-sm font-bold py-2 px-6 uppercase">ssh keys</h2> 32 34 <section class="rounded bg-white drop-shadow-sm px-6 py-4 mb-6 w-full lg:w-fit"> 33 35 <div id="key-list" class="flex flex-col gap-6 mb-8"> 34 36 {{ range .PubKeys }}
+40 -6
appview/state/repo.go
··· 404 404 return 405 405 } 406 406 407 + pullOwnerIdent, err := s.resolver.ResolveIdent(r.Context(), pr.OwnerDid) 408 + if err != nil { 409 + log.Println("failed to resolve pull owner", err) 410 + } 411 + 407 412 identsToResolve := make([]string, len(comments)) 408 413 for i, comment := range comments { 409 414 identsToResolve[i] = comment.OwnerDid ··· 418 423 } 419 424 } 420 425 421 - s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 422 - LoggedInUser: user, 423 - RepoInfo: f.RepoInfo(s, user), 424 - Pull: *pr, 425 - Comments: comments, 426 + secret, err := db.GetRegistrationKey(s.db, f.Knot) 427 + if err != nil { 428 + log.Printf("failed to get registration key for %s", f.Knot) 429 + s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.") 430 + return 431 + } 426 432 427 - DidHandleMap: didHandleMap, 433 + var mergeCheckResponse types.MergeCheckResponse 434 + ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 435 + if err == nil { 436 + resp, err := ksClient.MergeCheck([]byte(pr.Patch), pr.OwnerDid, f.RepoName, pr.TargetBranch) 437 + if err != nil { 438 + log.Println("failed to check for mergeability:", err) 439 + } else { 440 + respBody, err := io.ReadAll(resp.Body) 441 + if err != nil { 442 + log.Println("failed to read merge check response body") 443 + } else { 444 + err = json.Unmarshal(respBody, &mergeCheckResponse) 445 + if err != nil { 446 + log.Println("failed to unmarshal merge check response", err) 447 + } 448 + } 449 + } 450 + } else { 451 + log.Printf("failed to setup signed client for %s; ignoring...", f.Knot) 452 + } 453 + 454 + s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 455 + LoggedInUser: user, 456 + RepoInfo: f.RepoInfo(s, user), 457 + Pull: *pr, 458 + Comments: comments, 459 + PullOwnerHandle: pullOwnerIdent.Handle.String(), 460 + DidHandleMap: didHandleMap, 461 + MergeCheck: mergeCheckResponse, 428 462 }) 429 463 } 430 464
+164
appview/state/router.go
··· 1 + package state 2 + 3 + import ( 4 + "net/http" 5 + "strings" 6 + 7 + "github.com/go-chi/chi/v5" 8 + ) 9 + 10 + func (s *State) Router() http.Handler { 11 + router := chi.NewRouter() 12 + 13 + router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 14 + pat := chi.URLParam(r, "*") 15 + if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") { 16 + s.UserRouter().ServeHTTP(w, r) 17 + } else { 18 + s.StandardRouter().ServeHTTP(w, r) 19 + } 20 + }) 21 + 22 + return router 23 + } 24 + 25 + func (s *State) UserRouter() http.Handler { 26 + r := chi.NewRouter() 27 + 28 + // strip @ from user 29 + r.Use(StripLeadingAt) 30 + 31 + r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) { 32 + r.Get("/", s.ProfilePage) 33 + r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) { 34 + r.Get("/", s.RepoIndex) 35 + r.Get("/commits/{ref}", s.RepoLog) 36 + r.Route("/tree/{ref}", func(r chi.Router) { 37 + r.Get("/", s.RepoIndex) 38 + r.Get("/*", s.RepoTree) 39 + }) 40 + r.Get("/commit/{ref}", s.RepoCommit) 41 + r.Get("/branches", s.RepoBranches) 42 + r.Get("/tags", s.RepoTags) 43 + r.Get("/blob/{ref}/*", s.RepoBlob) 44 + 45 + r.Route("/issues", func(r chi.Router) { 46 + r.Get("/", s.RepoIssues) 47 + r.Get("/{issue}", s.RepoSingleIssue) 48 + 49 + r.Group(func(r chi.Router) { 50 + r.Use(AuthMiddleware(s)) 51 + r.Get("/new", s.NewIssue) 52 + r.Post("/new", s.NewIssue) 53 + r.Post("/{issue}/comment", s.IssueComment) 54 + r.Post("/{issue}/close", s.CloseIssue) 55 + r.Post("/{issue}/reopen", s.ReopenIssue) 56 + }) 57 + }) 58 + 59 + r.Route("/pulls", func(r chi.Router) { 60 + r.Get("/", s.RepoPulls) 61 + r.Get("/{pull}", s.RepoSinglePull) 62 + 63 + r.Group(func(r chi.Router) { 64 + r.Use(AuthMiddleware(s)) 65 + r.Get("/new", s.NewPull) 66 + r.Post("/new", s.NewPull) 67 + // r.Post("/{pull}/comment", s.PullComment) 68 + // r.Post("/{pull}/close", s.ClosePull) 69 + // r.Post("/{pull}/reopen", s.ReopenPull) 70 + // r.Post("/{pull}/merge", s.MergePull) 71 + }) 72 + }) 73 + 74 + // These routes get proxied to the knot 75 + r.Get("/info/refs", s.InfoRefs) 76 + r.Post("/git-upload-pack", s.UploadPack) 77 + 78 + // settings routes, needs auth 79 + r.Group(func(r chi.Router) { 80 + r.Use(AuthMiddleware(s)) 81 + // repo description can only be edited by owner 82 + r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) { 83 + r.Put("/", s.RepoDescription) 84 + r.Get("/", s.RepoDescription) 85 + r.Get("/edit", s.RepoDescriptionEdit) 86 + }) 87 + r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 88 + r.Get("/", s.RepoSettings) 89 + r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator) 90 + }) 91 + }) 92 + }) 93 + }) 94 + 95 + r.NotFound(func(w http.ResponseWriter, r *http.Request) { 96 + s.pages.Error404(w) 97 + }) 98 + 99 + return r 100 + } 101 + 102 + func (s *State) StandardRouter() http.Handler { 103 + r := chi.NewRouter() 104 + 105 + r.Handle("/static/*", s.pages.Static()) 106 + 107 + r.Get("/", s.Timeline) 108 + 109 + r.With(AuthMiddleware(s)).Get("/logout", s.Logout) 110 + 111 + r.Route("/login", func(r chi.Router) { 112 + r.Get("/", s.Login) 113 + r.Post("/", s.Login) 114 + }) 115 + 116 + r.Route("/knots", func(r chi.Router) { 117 + r.Use(AuthMiddleware(s)) 118 + r.Get("/", s.Knots) 119 + r.Post("/key", s.RegistrationKey) 120 + 121 + r.Route("/{domain}", func(r chi.Router) { 122 + r.Post("/init", s.InitKnotServer) 123 + r.Get("/", s.KnotServerInfo) 124 + r.Route("/member", func(r chi.Router) { 125 + r.Use(RoleMiddleware(s, "server:owner")) 126 + r.Get("/", s.ListMembers) 127 + r.Put("/", s.AddMember) 128 + r.Delete("/", s.RemoveMember) 129 + }) 130 + }) 131 + }) 132 + 133 + r.Route("/repo", func(r chi.Router) { 134 + r.Route("/new", func(r chi.Router) { 135 + r.Use(AuthMiddleware(s)) 136 + r.Get("/", s.NewRepo) 137 + r.Post("/", s.NewRepo) 138 + }) 139 + // r.Post("/import", s.ImportRepo) 140 + }) 141 + 142 + r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) { 143 + r.Post("/", s.Follow) 144 + r.Delete("/", s.Follow) 145 + }) 146 + 147 + r.With(AuthMiddleware(s)).Route("/star", func(r chi.Router) { 148 + r.Post("/", s.Star) 149 + r.Delete("/", s.Star) 150 + }) 151 + 152 + r.Route("/settings", func(r chi.Router) { 153 + r.Use(AuthMiddleware(s)) 154 + r.Get("/", s.Settings) 155 + r.Put("/keys", s.SettingsKeys) 156 + }) 157 + 158 + r.Get("/keys/{user}", s.Keys) 159 + 160 + r.NotFound(func(w http.ResponseWriter, r *http.Request) { 161 + s.pages.Error404(w) 162 + }) 163 + return r 164 + }
-157
appview/state/state.go
··· 829 829 830 830 return fmt.Sprintf("https://cdn.bsky.app/img/feed_thumbnail/plain/%s/%s", did, link), nil 831 831 } 832 - 833 - func (s *State) Router() http.Handler { 834 - router := chi.NewRouter() 835 - 836 - router.HandleFunc("/*", func(w http.ResponseWriter, r *http.Request) { 837 - pat := chi.URLParam(r, "*") 838 - if strings.HasPrefix(pat, "did:") || strings.HasPrefix(pat, "@") { 839 - s.UserRouter().ServeHTTP(w, r) 840 - } else { 841 - s.StandardRouter().ServeHTTP(w, r) 842 - } 843 - }) 844 - 845 - return router 846 - } 847 - 848 - func (s *State) UserRouter() http.Handler { 849 - r := chi.NewRouter() 850 - 851 - // strip @ from user 852 - r.Use(StripLeadingAt) 853 - 854 - r.With(ResolveIdent(s)).Route("/{user}", func(r chi.Router) { 855 - r.Get("/", s.ProfilePage) 856 - r.With(ResolveRepoKnot(s)).Route("/{repo}", func(r chi.Router) { 857 - r.Get("/", s.RepoIndex) 858 - r.Get("/commits/{ref}", s.RepoLog) 859 - r.Route("/tree/{ref}", func(r chi.Router) { 860 - r.Get("/", s.RepoIndex) 861 - r.Get("/*", s.RepoTree) 862 - }) 863 - r.Get("/commit/{ref}", s.RepoCommit) 864 - r.Get("/branches", s.RepoBranches) 865 - r.Get("/tags", s.RepoTags) 866 - r.Get("/blob/{ref}/*", s.RepoBlob) 867 - 868 - r.Route("/issues", func(r chi.Router) { 869 - r.Get("/", s.RepoIssues) 870 - r.Get("/{issue}", s.RepoSingleIssue) 871 - 872 - r.Group(func(r chi.Router) { 873 - r.Use(AuthMiddleware(s)) 874 - r.Get("/new", s.NewIssue) 875 - r.Post("/new", s.NewIssue) 876 - r.Post("/{issue}/comment", s.IssueComment) 877 - r.Post("/{issue}/close", s.CloseIssue) 878 - r.Post("/{issue}/reopen", s.ReopenIssue) 879 - }) 880 - }) 881 - 882 - r.Route("/pulls", func(r chi.Router) { 883 - r.Get("/", s.RepoPulls) 884 - r.Get("/{pull}", s.RepoSinglePull) 885 - 886 - r.Group(func(r chi.Router) { 887 - r.Use(AuthMiddleware(s)) 888 - r.Get("/new", s.NewPull) 889 - r.Post("/new", s.NewPull) 890 - // r.Post("/{pull}/comment", s.PullComment) 891 - // r.Post("/{pull}/close", s.ClosePull) 892 - // r.Post("/{pull}/reopen", s.ReopenPull) 893 - // r.Post("/{pull}/merge", s.MergePull) 894 - }) 895 - }) 896 - 897 - // These routes get proxied to the knot 898 - r.Get("/info/refs", s.InfoRefs) 899 - r.Post("/git-upload-pack", s.UploadPack) 900 - 901 - // settings routes, needs auth 902 - r.Group(func(r chi.Router) { 903 - r.Use(AuthMiddleware(s)) 904 - // repo description can only be edited by owner 905 - r.With(RepoPermissionMiddleware(s, "repo:owner")).Route("/description", func(r chi.Router) { 906 - r.Put("/", s.RepoDescription) 907 - r.Get("/", s.RepoDescription) 908 - r.Get("/edit", s.RepoDescriptionEdit) 909 - }) 910 - r.With(RepoPermissionMiddleware(s, "repo:settings")).Route("/settings", func(r chi.Router) { 911 - r.Get("/", s.RepoSettings) 912 - r.With(RepoPermissionMiddleware(s, "repo:invite")).Put("/collaborator", s.AddCollaborator) 913 - }) 914 - }) 915 - }) 916 - }) 917 - 918 - r.NotFound(func(w http.ResponseWriter, r *http.Request) { 919 - s.pages.Error404(w) 920 - }) 921 - 922 - return r 923 - } 924 - 925 - func (s *State) StandardRouter() http.Handler { 926 - r := chi.NewRouter() 927 - 928 - r.Handle("/static/*", s.pages.Static()) 929 - 930 - r.Get("/", s.Timeline) 931 - 932 - r.With(AuthMiddleware(s)).Get("/logout", s.Logout) 933 - 934 - r.Route("/login", func(r chi.Router) { 935 - r.Get("/", s.Login) 936 - r.Post("/", s.Login) 937 - }) 938 - 939 - r.Route("/knots", func(r chi.Router) { 940 - r.Use(AuthMiddleware(s)) 941 - r.Get("/", s.Knots) 942 - r.Post("/key", s.RegistrationKey) 943 - 944 - r.Route("/{domain}", func(r chi.Router) { 945 - r.Post("/init", s.InitKnotServer) 946 - r.Get("/", s.KnotServerInfo) 947 - r.Route("/member", func(r chi.Router) { 948 - r.Use(RoleMiddleware(s, "server:owner")) 949 - r.Get("/", s.ListMembers) 950 - r.Put("/", s.AddMember) 951 - r.Delete("/", s.RemoveMember) 952 - }) 953 - }) 954 - }) 955 - 956 - r.Route("/repo", func(r chi.Router) { 957 - r.Route("/new", func(r chi.Router) { 958 - r.Use(AuthMiddleware(s)) 959 - r.Get("/", s.NewRepo) 960 - r.Post("/", s.NewRepo) 961 - }) 962 - // r.Post("/import", s.ImportRepo) 963 - }) 964 - 965 - r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) { 966 - r.Post("/", s.Follow) 967 - r.Delete("/", s.Follow) 968 - }) 969 - 970 - r.With(AuthMiddleware(s)).Route("/star", func(r chi.Router) { 971 - r.Post("/", s.Star) 972 - r.Delete("/", s.Star) 973 - }) 974 - 975 - r.Route("/settings", func(r chi.Router) { 976 - r.Use(AuthMiddleware(s)) 977 - r.Get("/", s.Settings) 978 - r.Put("/keys", s.SettingsKeys) 979 - r.Delete("/keys", s.SettingsKeys) 980 - }) 981 - 982 - r.Get("/keys/{user}", s.Keys) 983 - 984 - r.NotFound(func(w http.ResponseWriter, r *http.Request) { 985 - s.pages.Error404(w) 986 - }) 987 - return r 988 - }
+5 -5
input.css
··· 110 110 font-size: 14px; 111 111 } 112 112 a { 113 - @apply no-underline text-black hover:underline hover:text-gray-800; 113 + @apply no-underline text-black hover:underline hover:text-gray-800; 114 114 } 115 115 116 116 label { 117 - @apply block text-sm text-black; 117 + @apply block mb-2 text-gray-900 text-sm font-bold py-2 uppercase; 118 118 } 119 119 input { 120 - @apply bg-white border border-gray-400 rounded-sm focus:ring-black p-2; 120 + @apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3; 121 121 } 122 122 textarea { 123 - @apply bg-white border border-gray-400 rounded-sm focus:ring-black p-2; 123 + @apply bg-white border border-gray-400 rounded-sm focus:ring-black p-3; 124 124 } 125 125 details summary::-webkit-details-marker { 126 - display: none; 126 + display: none; 127 127 } 128 128 } 129 129
+4 -1
knotserver/routes.go
··· 628 628 629 629 err = gr.MergeCheck([]byte(patch), branch) 630 630 if err == nil { 631 - w.WriteHeader(http.StatusOK) 631 + response := types.MergeCheckResponse{ 632 + IsConflicted: false, 633 + } 634 + writeJSON(w, response) 632 635 return 633 636 } 634 637