Monorepo for Tangled tangled.org

appview/pulls: improve ui + validate patches

authored by anirudh.fi and committed by oppi.li a588049f c8070ec1

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 }} 5 6 {{ define "repoContent" }} 7 <header class="pb-4">
··· 1 + {{ define "title" }}{{ .Issue.Title }} &middot; issue #{{ .Issue.IssueId }} &middot;{{ .RepoInfo.FullName }}{{ end }} 2 3 {{ define "repoContent" }} 4 <header class="pb-4">
+15 -9
appview/pages/templates/repo/pulls/new.html
··· 15 <p class="text-gray-500"> 16 The branch you want to make your change against. 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 }} 27 </select> 28 </div> 29 <div> ··· 44 rows="10" 45 class="w-full resize-y font-mono" 46 placeholder="Paste your git-format-patch output here." 47 ></textarea> 48 </div> 49 </div> 50 <div>
··· 15 <p class="text-gray-500"> 16 The branch you want to make your change against. 17 </p> 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 }} 28 </select> 29 </div> 30 <div> ··· 45 rows="10" 46 class="w-full resize-y font-mono" 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']" 52 ></textarea> 53 + <div id="pull-validate"></div> 54 </div> 55 </div> 56 <div>
+29 -38
appview/pages/templates/repo/pulls/pull.html
··· 69 {{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} 70 {{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }} 71 72 - {{ if $isPullAuthor }} 73 <section id="update-card" class="mt-8 space-y-4 relative"> 74 {{ block "resubmitCard" . }} {{ end }} 75 </section> ··· 87 {{ end }} 88 </section> 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 <div id="pull-close"></div> 109 <div id="pull-reopen"></div> 110 {{ end }} ··· 202 <div 203 id="merge-status-card" 204 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 216 <div class="flex items-center gap-2 text-purple-500"> 217 <i data-lucide="git-merge" class="w-4 h-4"></i> 218 <span class="font-medium" 219 - >Pull request successfully merged</span 220 > 221 </div> 222 ··· 263 264 {{ define "noConflictsCard" }} 265 {{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }} 266 <div 267 id="merge-status-card" 268 class="rounded relative border bg-green-50 border-green-200 p-4"> ··· 291 </button> 292 {{ end }} 293 294 <div id="pull-merge-error" class="error"></div> 295 <div id="pull-merge-success" class="success"></div> 296 </div> ··· 304 305 <div class="flex items-center gap-2 text-amber-500"> 306 <i data-lucide="edit" class="w-4 h-4"></i> 307 - <span class="font-medium">Resubmit your patch</span> 308 </div> 309 310 <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. 314 </div> 315 316 - <div class="mt-4 flex items-center gap-2"> 317 - <form hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" class="w-full"> 318 <textarea 319 name="patch" 320 - class="w-full p-2 rounded border border-gray-200" 321 - placeholder="Enter new patch" 322 ></textarea> 323 <button 324 type="submit"
··· 69 {{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} 70 {{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }} 71 72 + {{ if and $isPullAuthor (not .Pull.State.IsMerged) }} 73 <section id="update-card" class="mt-8 space-y-4 relative"> 74 {{ block "resubmitCard" . }} {{ end }} 75 </section> ··· 87 {{ end }} 88 </section> 89 90 <div id="pull-close"></div> 91 <div id="pull-reopen"></div> 92 {{ end }} ··· 184 <div 185 id="merge-status-card" 186 class="rounded relative bg-purple-50 border border-purple-200 p-4"> 187 188 <div class="flex items-center gap-2 text-purple-500"> 189 <i data-lucide="git-merge" class="w-4 h-4"></i> 190 <span class="font-medium" 191 + >pull request successfully merged</span 192 > 193 </div> 194 ··· 235 236 {{ define "noConflictsCard" }} 237 {{ $isPushAllowed := .RepoInfo.Roles.IsPushAllowed }} 238 + {{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} 239 <div 240 id="merge-status-card" 241 class="rounded relative border bg-green-50 border-green-200 p-4"> ··· 264 </button> 265 {{ end }} 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 + 285 <div id="pull-merge-error" class="error"></div> 286 <div id="pull-merge-success" class="success"></div> 287 </div> ··· 295 296 <div class="flex items-center gap-2 text-amber-500"> 297 <i data-lucide="edit" class="w-4 h-4"></i> 298 + <span class="font-medium">resubmit your patch</span> 299 </div> 300 301 <div class="mt-2 text-sm text-gray-700"> 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. 305 </div> 306 307 + <div class="mt-4 flex flex-col"> 308 + <form hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" class="w-full" hx-swap="none"> 309 <textarea 310 name="patch" 311 + class="w-full p-2 mb-2 rounded border border-gray-200" 312 + placeholder="Paste your updated patch here." 313 ></textarea> 314 <button 315 type="submit"
+37 -1
appview/state/pull.go
··· 7 "log" 8 "net/http" 9 "strconv" 10 "time" 11 12 "github.com/sotangled/tangled/api/tangled" ··· 300 return 301 } 302 303 tx, err := s.db.BeginTx(r.Context(), nil) 304 if err != nil { 305 log.Println("failed to start tx") ··· 392 return 393 } 394 395 tx, err := s.db.BeginTx(r.Context(), nil) 396 if err != nil { 397 log.Println("failed to start tx") ··· 478 } 479 480 // Merge the pull request 481 - resp, err := ksClient.Merge([]byte(pull.LatestPatch()), user.Did, f.RepoName, pull.TargetBranch) 482 if err != nil { 483 log.Printf("failed to merge pull request: %s", err) 484 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 607 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 608 return 609 }
··· 7 "log" 8 "net/http" 9 "strconv" 10 + "strings" 11 "time" 12 13 "github.com/sotangled/tangled/api/tangled" ··· 301 return 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 + 310 tx, err := s.db.BeginTx(r.Context(), nil) 311 if err != nil { 312 log.Println("failed to start tx") ··· 399 return 400 } 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 + 408 tx, err := s.db.BeginTx(r.Context(), nil) 409 if err != nil { 410 log.Println("failed to start tx") ··· 491 } 492 493 // Merge the pull request 494 + resp, err := ksClient.Merge([]byte(pull.LatestPatch()), user.Did, f.RepoName, pull.TargetBranch, pull.Title, pull.Body, "", "") 495 if err != nil { 496 log.Printf("failed to merge pull request: %s", err) 497 s.pages.Notice(w, "pull-merge-error", "Failed to merge pull request. Try again later.") ··· 620 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pull.PullId)) 621 return 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 "net/http" 11 "net/url" 12 "time" 13 ) 14 15 type SignerTransport struct { ··· 156 return s.client.Do(req) 157 } 158 159 - func (s *SignedClient) Merge(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) { 160 const ( 161 Method = "POST" 162 ) 163 endpoint := fmt.Sprintf("/%s/%s/merge", ownerDid, targetRepo) 164 165 - body, _ := json.Marshal(map[string]interface{}{ 166 - "patch": string(patch), 167 - "branch": branch, 168 - }) 169 170 req, err := s.newRequest(Method, endpoint, body) 171 if err != nil {
··· 10 "net/http" 11 "net/url" 12 "time" 13 + 14 + "github.com/sotangled/tangled/types" 15 ) 16 17 type SignerTransport struct { ··· 158 return s.client.Do(req) 159 } 160 161 + func (s *SignedClient) Merge( 162 + patch []byte, 163 + ownerDid, targetRepo, branch, commitMessage, commitBody, authorName, authorEmail string, 164 + ) (*http.Response, error) { 165 const ( 166 Method = "POST" 167 ) 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 + } 178 179 + body, _ := json.Marshal(mr) 180 181 req, err := s.newRequest(Method, endpoint, body) 182 if err != nil {