Monorepo for Tangled tangled.org

appview/pulls: improve ui + validate patches

anirudh.fi f0e33f3c 4b4d4a6d

verified
Changed files
+98 -57
appview
pages
templates
repo
state
+1 -4
appview/pages/templates/repo/issues/issue.html
··· 1 - {{ define "title" }} 2 - {{ .Issue.Title }} &middot; issue #{{ .Issue.IssueId }} &middot; 3 - {{ .RepoInfo.FullName }} 4 - {{ end }} 1 + {{ define "title" }}{{ .Issue.Title }} &middot; issue #{{ .Issue.IssueId }} &middot;{{ .RepoInfo.FullName }}{{ end }} 5 2 6 3 {{ define "repoContent" }} 7 4 <header class="pb-4">
+15 -9
appview/pages/templates/repo/pulls/new.html
··· 15 15 <p class="text-gray-500"> 16 16 The branch you want to make your change against. 17 17 </p> 18 - <select name="targetBranch" class="p-1 border border-gray-200 bg-white"> 19 - <option disabled selected>select a branch</option> 20 - {{ range .Branches }} 21 - <option 22 - value="{{ .Reference.Name }}" 23 - class="py-1"> 24 - {{ .Reference.Name }} 25 - </option> 26 - {{ end }} 18 + <select 19 + name="targetBranch" 20 + class="p-1 border border-gray-200 bg-white" 21 + > 22 + <option disabled selected>select a branch</option> 23 + {{ range .Branches }} 24 + <option value="{{ .Reference.Name }}" class="py-1"> 25 + {{ .Reference.Name }} 26 + </option> 27 + {{ end }} 27 28 </select> 28 29 </div> 29 30 <div> ··· 44 45 rows="10" 45 46 class="w-full resize-y font-mono" 46 47 placeholder="Paste your git-format-patch output here." 48 + hx-post="/pulls/validate" 49 + hx-trigger="input changed delay:500ms" 50 + hx-target="#patch-validation" 51 + hx-include="[name='patch']" 47 52 ></textarea> 53 + <div id="pull-validate"></div> 48 54 </div> 49 55 </div> 50 56 <div>
+29 -38
appview/pages/templates/repo/pulls/pull.html
··· 69 69 {{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} 70 70 {{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }} 71 71 72 - {{ if $isPullAuthor }} 72 + {{ if and $isPullAuthor (not .Pull.State.IsMerged) }} 73 73 <section id="update-card" class="mt-8 space-y-4 relative"> 74 74 {{ block "resubmitCard" . }} {{ end }} 75 75 </section> ··· 87 87 {{ end }} 88 88 </section> 89 89 90 - {{ if and (or $isPullAuthor $isPushAllowed) (not .Pull.State.IsMerged) }} 91 - {{ $action := "close" }} 92 - {{ $icon := "circle-x" }} 93 - {{ $hoverColor := "red" }} 94 - {{ if .Pull.State.IsClosed }} 95 - {{ $action = "reopen" }} 96 - {{ $icon = "circle-dot" }} 97 - {{ $hoverColor = "green" }} 98 - {{ end }} 99 - <button 100 - hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/{{ $action }}" 101 - hx-swap="none" 102 - class="btn mt-8 text-sm flex items-center gap-2"> 103 - <i data-lucide="{{ $icon }}" class="w-4 h-4 mr-2 text-{{ $hoverColor }}-400"></i> 104 - <span class="text-black">{{ $action }}</span> 105 - </button> 106 - {{ end }} 107 - 108 90 <div id="pull-close"></div> 109 91 <div id="pull-reopen"></div> 110 92 {{ end }} ··· 202 184 <div 203 185 id="merge-status-card" 204 186 class="rounded relative bg-purple-50 border border-purple-200 p-4"> 205 - {{ if gt (len .Comments) 0 }} 206 - <div 207 - class="absolute left-8 -top-4 w-px h-4 bg-gray-300" 208 - ></div> 209 - {{ else }} 210 - <div 211 - class="absolute left-8 -top-8 w-px h-8 bg-gray-300" 212 - ></div> 213 - {{ end }} 214 - 215 187 216 188 <div class="flex items-center gap-2 text-purple-500"> 217 189 <i data-lucide="git-merge" class="w-4 h-4"></i> 218 190 <span class="font-medium" 219 - >Pull request successfully merged</span 191 + >pull request successfully merged</span 220 192 > 221 193 </div> 222 194 ··· 263 235 264 236 {{ define "noConflictsCard" }} 265 237 {{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }} 238 + {{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} 266 239 <div 267 240 id="merge-status-card" 268 241 class="rounded relative border bg-green-50 border-green-200 p-4"> ··· 291 264 </button> 292 265 {{ end }} 293 266 267 + {{ if and (or $isPullAuthor $isPushAllowed) (not .Pull.State.IsMerged) }} 268 + {{ $action := "close" }} 269 + {{ $icon := "circle-x" }} 270 + {{ $hoverColor := "red" }} 271 + {{ if .Pull.State.IsClosed }} 272 + {{ $action = "reopen" }} 273 + {{ $icon = "circle-dot" }} 274 + {{ $hoverColor = "green" }} 275 + {{ end }} 276 + <button 277 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/{{ $action }}" 278 + hx-swap="none" 279 + class="btn mt-4 flex items-center gap-2"> 280 + <i data-lucide="{{ $icon }}" class="w-4 h-4 text-{{ $hoverColor }}-400"></i> 281 + <span class="text-black">{{ $action }}</span> 282 + </button> 283 + {{ end }} 284 + 294 285 <div id="pull-merge-error" class="error"></div> 295 286 <div id="pull-merge-success" class="success"></div> 296 287 </div> ··· 304 295 305 296 <div class="flex items-center gap-2 text-amber-500"> 306 297 <i data-lucide="edit" class="w-4 h-4"></i> 307 - <span class="font-medium">Resubmit your patch</span> 298 + <span class="font-medium">resubmit your patch</span> 308 299 </div> 309 300 310 301 <div class="mt-2 text-sm text-gray-700"> 311 - You can update this patch to address reviews if any. 312 - This begins a new round of reviews, 313 - you can still view your previous submissions and reviews. 302 + You can update this patch to address any reviews. 303 + This will begin a new round of reviews, 304 + but you'll still be able to view your previous submissions and feedback. 314 305 </div> 315 306 316 - <div class="mt-4 flex items-center gap-2"> 317 - <form hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" class="w-full"> 307 + <div class="mt-4 flex flex-col"> 308 + <form hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" class="w-full" hx-swap="none"> 318 309 <textarea 319 310 name="patch" 320 - class="w-full p-2 rounded border border-gray-200" 321 - placeholder="Enter new patch" 311 + class="w-full p-2 mb-2 rounded border border-gray-200" 312 + placeholder="Paste your updated patch here." 322 313 ></textarea> 323 314 <button 324 315 type="submit"
+37 -1
appview/state/pull.go
··· 7 7 "log" 8 8 "net/http" 9 9 "strconv" 10 + "strings" 10 11 "time" 11 12 12 13 "github.com/sotangled/tangled/api/tangled" ··· 300 301 return 301 302 } 302 303 304 + // Validate patch format 305 + if !isPatchValid(patch) { 306 + s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") 307 + return 308 + } 309 + 303 310 tx, err := s.db.BeginTx(r.Context(), nil) 304 311 if err != nil { 305 312 log.Println("failed to start tx") ··· 392 399 return 393 400 } 394 401 402 + // Validate patch format 403 + if !isPatchValid(patch) { 404 + s.pages.Notice(w, "resubmit-error", "Invalid patch format. Please provide a valid diff.") 405 + return 406 + } 407 + 395 408 tx, err := s.db.BeginTx(r.Context(), nil) 396 409 if err != nil { 397 410 log.Println("failed to start tx") ··· 478 491 } 479 492 480 493 // Merge the pull request 481 - resp, err := ksClient.Merge([]byte(pull.LatestPatch()), user.Did, f.RepoName, pull.TargetBranch) 494 + resp, err := ksClient.Merge([]byte(pull.LatestPatch()), user.Did, f.RepoName, pull.TargetBranch, pull.Title, pull.Body, "", "") 482 495 if err != nil { 483 496 log.Printf("failed to merge pull request: %s", err) 484 497 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 607 620 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 608 621 return 609 622 } 623 + 624 + // Very basic validation to check if it looks like a diff/patch 625 + // A valid patch usually starts with diff or --- lines 626 + func isPatchValid(patch string) bool { 627 + // Basic validation to check if it looks like a diff/patch 628 + // A valid patch usually starts with diff or --- lines 629 + if len(patch) == 0 { 630 + return false 631 + } 632 + 633 + lines := strings.Split(patch, "\n") 634 + if len(lines) < 2 { 635 + return false 636 + } 637 + 638 + // Check for common patch format markers 639 + firstLine := strings.TrimSpace(lines[0]) 640 + return strings.HasPrefix(firstLine, "diff ") || 641 + strings.HasPrefix(firstLine, "--- ") || 642 + strings.HasPrefix(firstLine, "Index: ") || 643 + strings.HasPrefix(firstLine, "+++ ") || 644 + strings.HasPrefix(firstLine, "@@ ") 645 + }
+16 -5
appview/state/signer.go
··· 10 10 "net/http" 11 11 "net/url" 12 12 "time" 13 + 14 + "github.com/sotangled/tangled/types" 13 15 ) 14 16 15 17 type SignerTransport struct { ··· 156 158 return s.client.Do(req) 157 159 } 158 160 159 - func (s *SignedClient) Merge(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) { 161 + func (s *SignedClient) Merge( 162 + patch []byte, 163 + ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string, 164 + ) (*http.Response, error) { 160 165 const ( 161 166 Method = "POST" 162 167 ) 163 168 endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo) 169 + 170 + mr := types.MergeRequest{ 171 + Branch: branch, 172 + CommitMessage: commitMessage, 173 + CommitBody: commitBody, 174 + AuthorName: authorName, 175 + AuthorEmail: authorEmail, 176 + Patch: string(patch), 177 + } 164 178 165 - body, _ := json.Marshal(map[string]interface{}{ 166 - "patch": string(patch), 167 - "branch": branch, 168 - }) 179 + body, _ := json.Marshal(mr) 169 180 170 181 req, err := s.newRequest(Method, endpoint, body) 171 182 if err != nil {