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

appview: implement patch updates in pulls

Changed files
+185 -56
appview
db
pages
templates
repo
issues
pulls
state
+5
appview/db/pulls.go
··· 254 255 return count, nil 256 }
··· 254 255 return count, nil 256 } 257 + 258 + func EditPatch(e Execer, repoAt syntax.ATURI, pullId int, patch string) error { 259 + _, err := e.Exec(`update pulls set patch = ? where repo_at = ? and pull_id = ?`, patch, repoAt, pullId) 260 + return err 261 + }
+4 -4
appview/pages/templates/repo/issues/issue.html
··· 1 {{ define "title" }} 2 - {{ .Issue.Title }} &middot; 3 {{ .RepoInfo.FullName }} 4 {{ end }} 5 6 {{ define "repoContent" }} 7 - <header> 8 - <p class="text-2xl"> 9 {{ .Issue.Title }} 10 <span class="text-gray-500">#{{ .Issue.IssueId }}</span> 11 - </p> 12 </header> 13 14 {{ $bgColor := "bg-gray-800" }}
··· 1 {{ define "title" }} 2 + {{ .Issue.Title }} &middot; issue #{{ .Issue.IssueId }} &middot; 3 {{ .RepoInfo.FullName }} 4 {{ end }} 5 6 {{ define "repoContent" }} 7 + <header class="pb-4"> 8 + <h1 class="text-2xl"> 9 {{ .Issue.Title }} 10 <span class="text-gray-500">#{{ .Issue.IssueId }}</span> 11 + </h1> 12 </header> 13 14 {{ $bgColor := "bg-gray-800" }}
+135 -34
appview/pages/templates/repo/pulls/pull.html
··· 4 {{ end }} 5 6 {{ define "repoContent" }} 7 - <h1> 8 - {{ .Pull.Title }} 9 - <span class="text-gray-400">#{{ .Pull.PullId }}</span> 10 - </h1> 11 {{ $bgColor := "bg-gray-800" }} 12 {{ $icon := "ban" }} 13 {{ if eq .State "open" }} ··· 49 {{ end }} 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> 94 {{ end }} 95 96 {{ define "repoAfter" }} ··· 134 </div> 135 </div> 136 {{ end }} 137 </section> 138 139 {{ if .LoggedInUser }} ··· 172 ></i> 173 <span class="text-black">{{ $action }}</span> 174 </button> 175 - <div id="pull-action" class="error"></div> 176 </form> 177 {{ end }} 178 {{ end }}
··· 4 {{ end }} 5 6 {{ define "repoContent" }} 7 + 8 + <header class="pb-4"> 9 + <h1 class="text-2xl"> 10 + {{ .Pull.Title }} 11 + <span class="text-gray-500">#{{ .Pull.PullId }}</span> 12 + </h1> 13 + </header> 14 + 15 {{ $bgColor := "bg-gray-800" }} 16 {{ $icon := "ban" }} 17 {{ if eq .State "open" }} ··· 53 {{ end }} 54 </section> 55 56 + <div class="flex flex-col justify-end mt-4"> 57 <details> 58 <summary 59 + 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 mt-auto" 60 > 61 <i data-lucide="code" class="w-4 h-4 mr-2"></i> 62 <span>patch</span> 63 </summary> 64 + <div class="relative"> 65 + <pre 66 + id="patch-preview" 67 + class="font-mono overflow-x-scroll bg-gray-50 p-4 rounded-b border border-gray-200 text-sm" 68 + > 69 {{- .Pull.Patch -}} 70 + </pre 71 + > 72 + <form 73 + id="patch-form" 74 + hx-patch="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/patch" 75 + hx-swap="none" 76 + > 77 + <textarea 78 + id="patch" 79 + name="patch" 80 + class="font-mono w-full h-full p-4 rounded-b border border-gray-200 text-sm hidden" 81 + > 82 + {{- .Pull.Patch -}}</textarea 83 + > 84 + 85 + <div class="flex gap-2 justify-end mt-2"> 86 + <button 87 + id="edit-patch-btn" 88 + type="button" 89 + class="btn btn-sm" 90 + onclick="togglePatchEdit(true)" 91 + > 92 + <i data-lucide="edit" class="w-4 h-4 mr-1"></i>Edit 93 + </button> 94 + <button 95 + id="save-patch-btn" 96 + type="submit" 97 + class="btn btn-sm bg-green-500 hidden" 98 + > 99 + <i data-lucide="save" class="w-4 h-4 mr-1"></i>Save 100 + </button> 101 + <button 102 + id="cancel-patch-btn" 103 + type="button" 104 + class="btn btn-sm bg-gray-300 hidden" 105 + onclick="togglePatchEdit(false)" 106 + > 107 + Cancel 108 + </button> 109 + </div> 110 + </form> 111 + 112 + <div id="pull-error" class="error"></div> 113 + <div id="pull-success" class="success"></div> 114 + </div> 115 + <script> 116 + function togglePatchEdit(editMode) { 117 + const preview = document.getElementById("patch-preview"); 118 + const editor = document.getElementById("patch"); 119 + const editBtn = document.getElementById("edit-patch-btn"); 120 + const saveBtn = document.getElementById("save-patch-btn"); 121 + const cancelBtn = 122 + document.getElementById("cancel-patch-btn"); 123 + 124 + if (editMode) { 125 + preview.classList.add("hidden"); 126 + editor.classList.remove("hidden"); 127 + editBtn.classList.add("hidden"); 128 + saveBtn.classList.remove("hidden"); 129 + cancelBtn.classList.remove("hidden"); 130 + } else { 131 + preview.classList.remove("hidden"); 132 + editor.classList.add("hidden"); 133 + editBtn.classList.remove("hidden"); 134 + saveBtn.classList.add("hidden"); 135 + cancelBtn.classList.add("hidden"); 136 + } 137 + } 138 + 139 + document 140 + .getElementById("save-patch-btn") 141 + .addEventListener("click", function () { 142 + togglePatchEdit(false); 143 + }); 144 + </script> 145 </details> 146 </div> 147 + 148 + {{ if .MergeCheck }} 149 + <div class="mt-4" id="merge-check"> 150 + <div 151 + class="rounded-sm border p-4 {{ if .MergeCheck.IsConflicted }} 152 + bg-red-50 border-red-200 153 {{ else }} 154 + bg-green-50 border-green-200 155 + {{ end }}" 156 + > 157 + <div 158 + class="flex items-center gap-2 rounded-sm {{ if .MergeCheck.IsConflicted }} 159 + text-red-500 160 + {{ else }} 161 + text-green-500 162 + {{ end }}" 163 + > 164 + {{ if .MergeCheck.IsConflicted }} 165 + <i data-lucide="alert-triangle" class="w-4 h-4"></i> 166 + <span class="font-medium" 167 + >merge conflicts detected</span 168 + > 169 + {{ else }} 170 + <i data-lucide="check-circle" class="w-4 h-4"></i> 171 + <span class="font-medium">ready to merge</span> 172 + {{ end }} 173 + </div> 174 + 175 + {{ if .MergeCheck.IsConflicted }} 176 + <div class="mt-2"> 177 + <ul class="text-sm space-y-1"> 178 + {{ range .MergeCheck.Conflicts }} 179 + <li class="flex items-center"> 180 + <i 181 + data-lucide="file-warning" 182 + class="w-3 h-3 mr-1.5 text-red-500" 183 + ></i> 184 + <span class="font-mono" 185 + >{{ slice .Filename 0 (sub (len .Filename) 2) }}</span 186 + > 187 + </li> 188 + {{ end }} 189 + </ul> 190 + </div> 191 {{ end }} 192 </div> 193 </div> 194 + {{ end }} 195 {{ end }} 196 197 {{ define "repoAfter" }} ··· 235 </div> 236 </div> 237 {{ end }} 238 + 239 </section> 240 241 {{ if .LoggedInUser }} ··· 274 ></i> 275 <span class="text-black">{{ $action }}</span> 276 </button> 277 </form> 278 {{ end }} 279 {{ end }}
+36 -14
appview/state/repo.go
··· 230 } 231 } 232 233 - // MergeCheck gets called async, every time the patch diff is updated in a pull. 234 - func (s *State) MergeCheck(w http.ResponseWriter, r *http.Request) { 235 user := s.auth.GetUser(r) 236 f, err := fullyResolvedRepo(r) 237 if err != nil { 238 log.Println("failed to get repo and knot", err) 239 - s.pages.Notice(w, "pull", "Failed to check mergeability. Try again later.") 240 return 241 } 242 243 patch := r.FormValue("patch") 244 - targetBranch := r.FormValue("targetBranch") 245 246 - if patch == "" || targetBranch == "" { 247 - s.pages.Notice(w, "pull", "Patch and target branch are required.") 248 return 249 } 250 251 secret, err := db.GetRegistrationKey(s.db, f.Knot) 252 if err != nil { 253 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 254 - s.pages.Notice(w, "pull", "Failed to check mergeability. Try again later.") 255 return 256 } 257 258 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 259 if err != nil { 260 log.Printf("failed to create signed client for %s", f.Knot) 261 - s.pages.Notice(w, "pull", "Failed to check mergeability. Try again later.") 262 return 263 } 264 265 resp, err := ksClient.MergeCheck([]byte(patch), user.Did, f.RepoName, targetBranch) 266 if err != nil { 267 log.Println("failed to check mergeability", err) 268 - s.pages.Notice(w, "pull", "Unable to check for mergeability. Try again later.") 269 return 270 } 271 272 respBody, err := io.ReadAll(resp.Body) 273 if err != nil { 274 log.Println("failed to read knotserver response body") 275 - s.pages.Notice(w, "pull", "Unable to check for mergeability. Try again later.") 276 return 277 } 278 ··· 280 err = json.Unmarshal(respBody, &mergeCheckResponse) 281 if err != nil { 282 log.Println("failed to unmarshal merge check response", err) 283 - s.pages.Notice(w, "pull", "Failed to check mergeability. Try again later.") 284 return 285 } 286 287 - // TODO: this has to return a html fragment 288 - w.Header().Set("Content-Type", "application/json") 289 - json.NewEncoder(w).Encode(mergeCheckResponse) 290 } 291 292 func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
··· 230 } 231 } 232 233 + func (s *State) EditPatch(w http.ResponseWriter, r *http.Request) { 234 user := s.auth.GetUser(r) 235 f, err := fullyResolvedRepo(r) 236 if err != nil { 237 log.Println("failed to get repo and knot", err) 238 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 239 + return 240 + } 241 + 242 + prId := chi.URLParam(r, "pull") 243 + prIdInt, err := strconv.Atoi(prId) 244 + if err != nil { 245 + http.Error(w, "bad pr id", http.StatusBadRequest) 246 + log.Println("failed to parse pr id", err) 247 return 248 } 249 250 patch := r.FormValue("patch") 251 + if patch == "" { 252 + s.pages.Notice(w, "pull-error", "Patch is required.") 253 + return 254 + } 255 256 + err = db.EditPatch(s.db, f.RepoAt, prIdInt, patch) 257 + if err != nil { 258 + log.Println("failed to update patch", err) 259 + s.pages.Notice(w, "pull-error", "Failed to edit patch. Try again later.") 260 return 261 } 262 263 + // Get target branch after patch update 264 + pull, _, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt) 265 + if err != nil { 266 + log.Println("failed to get pull information", err) 267 + s.pages.Notice(w, "pull-success", "Patch updated successfully.") 268 + return 269 + } 270 + 271 + targetBranch := pull.TargetBranch 272 + 273 + // Perform merge check 274 secret, err := db.GetRegistrationKey(s.db, f.Knot) 275 if err != nil { 276 log.Printf("no key found for domain %s: %s\n", f.Knot, err) 277 + s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 278 return 279 } 280 281 ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev) 282 if err != nil { 283 log.Printf("failed to create signed client for %s", f.Knot) 284 + s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 285 return 286 } 287 288 resp, err := ksClient.MergeCheck([]byte(patch), user.Did, f.RepoName, targetBranch) 289 if err != nil { 290 log.Println("failed to check mergeability", err) 291 + s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 292 return 293 } 294 295 respBody, err := io.ReadAll(resp.Body) 296 if err != nil { 297 log.Println("failed to read knotserver response body") 298 + s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 299 return 300 } 301 ··· 303 err = json.Unmarshal(respBody, &mergeCheckResponse) 304 if err != nil { 305 log.Println("failed to unmarshal merge check response", err) 306 + s.pages.Notice(w, "pull-success", "Patch updated successfully, but couldn't check mergeability.") 307 return 308 } 309 310 + s.pages.HxLocation(w, fmt.Sprintf("/@%s/%s/pulls/%d", f.OwnerHandle(), f.RepoName, prIdInt)) 311 + return 312 } 313 314 func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
+1
appview/state/router.go
··· 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)
··· 64 r.Use(AuthMiddleware(s)) 65 r.Get("/new", s.NewPull) 66 r.Post("/new", s.NewPull) 67 + r.Patch("/{pull}/patch", s.EditPatch) 68 // r.Post("/{pull}/comment", s.PullComment) 69 // r.Post("/{pull}/close", s.ClosePull) 70 // r.Post("/{pull}/reopen", s.ReopenPull)
+4 -4
input.css
··· 105 106 @layer base { 107 html { 108 - letter-spacing: -0.01em; 109 - word-spacing: -0.07em; 110 - font-size: 14px; 111 } 112 a { 113 @apply no-underline text-black hover:underline hover:text-gray-800; ··· 147 @apply py-1 text-red-400; 148 } 149 .success { 150 - @apply py-1 text-black; 151 } 152 } 153 }
··· 105 106 @layer base { 107 html { 108 + letter-spacing: -0.01em; 109 + word-spacing: -0.07em; 110 + font-size: 14px; 111 } 112 a { 113 @apply no-underline text-black hover:underline hover:text-gray-800; ··· 147 @apply py-1 text-red-400; 148 } 149 .success { 150 + @apply py-1 text-green-400; 151 } 152 } 153 }