forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

add resubmit hints

unless a branch is updated, resubmit will remain disabled; and when
there are changes added to the branch, a hint is supplied.

Changed files
+230 -52
appview
db
pages
templates
fragments
repo
pulls
state
knotserver
types
+2 -1
appview/db/db.go
··· 264 264 return err 265 265 }) 266 266 267 - runMigration(db, "add-source-info-to-pulls", func(tx *sql.Tx) error { 267 + runMigration(db, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error { 268 268 _, err := tx.Exec(` 269 269 alter table pulls add column source_branch text; 270 270 alter table pulls add column source_repo_at text; 271 + alter table pull_submissions add column source_rev text; 271 272 `) 272 273 return err 273 274 })
+15 -8
appview/db/pulls.go
··· 83 83 RoundNumber int 84 84 Patch string 85 85 Comments []PullComment 86 + SourceRev string // include the rev that was used to create this submission: only for branch PRs 86 87 87 88 // meta 88 89 Created time.Time ··· 222 223 } 223 224 224 225 _, err = tx.Exec(` 225 - insert into pull_submissions (pull_id, repo_at, round_number, patch) 226 - values (?, ?, ?, ?) 227 - `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch) 226 + insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev) 227 + values (?, ?, ?, ?, ?) 228 + `, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev) 228 229 if err != nil { 229 230 return err 230 231 } ··· 395 396 396 397 submissionsQuery := ` 397 398 select 398 - id, pull_id, repo_at, round_number, patch, created 399 + id, pull_id, repo_at, round_number, patch, created, source_rev 399 400 from 400 401 pull_submissions 401 402 where ··· 412 413 for submissionsRows.Next() { 413 414 var submission PullSubmission 414 415 var submissionCreatedStr string 416 + var submissionSourceRev sql.NullString 415 417 err := submissionsRows.Scan( 416 418 &submission.ID, 417 419 &submission.PullId, ··· 419 421 &submission.RoundNumber, 420 422 &submission.Patch, 421 423 &submissionCreatedStr, 424 + &submissionSourceRev, 422 425 ) 423 426 if err != nil { 424 427 return nil, err ··· 430 433 } 431 434 submission.Created = submissionCreatedTime 432 435 436 + if submissionSourceRev.Valid { 437 + submission.SourceRev = submissionSourceRev.String 438 + } 439 + 433 440 submissionsMap[submission.ID] = &submission 434 441 } 435 442 if err = submissionsRows.Close(); err != nil { ··· 604 611 return err 605 612 } 606 613 607 - func ResubmitPull(e Execer, pull *Pull, newPatch string) error { 614 + func ResubmitPull(e Execer, pull *Pull, newPatch, sourceRev string) error { 608 615 newRoundNumber := len(pull.Submissions) 609 616 _, err := e.Exec(` 610 - insert into pull_submissions (pull_id, repo_at, round_number, patch) 611 - values (?, ?, ?, ?) 612 - `, pull.PullId, pull.RepoAt, newRoundNumber, newPatch) 617 + insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev) 618 + values (?, ?, ?, ?, ?) 619 + `, pull.PullId, pull.RepoAt, newRoundNumber, newPatch, sourceRev) 613 620 614 621 return err 615 622 }
+31 -11
appview/pages/pages.go
··· 586 586 return p.executeRepo("repo/pulls/pulls", w, params) 587 587 } 588 588 589 + type ResubmitResult uint64 590 + 591 + const ( 592 + ShouldResubmit ResubmitResult = iota 593 + ShouldNotResubmit 594 + Unknown 595 + ) 596 + 597 + func (r ResubmitResult) Yes() bool { 598 + return r == ShouldResubmit 599 + } 600 + func (r ResubmitResult) No() bool { 601 + return r == ShouldNotResubmit 602 + } 603 + func (r ResubmitResult) Unknown() bool { 604 + return r == Unknown 605 + } 606 + 589 607 type RepoSinglePullParams struct { 590 - LoggedInUser *auth.User 591 - RepoInfo RepoInfo 592 - Active string 593 - DidHandleMap map[string]string 594 - Pull *db.Pull 595 - MergeCheck types.MergeCheckResponse 608 + LoggedInUser *auth.User 609 + RepoInfo RepoInfo 610 + Active string 611 + DidHandleMap map[string]string 612 + Pull *db.Pull 613 + MergeCheck types.MergeCheckResponse 614 + ResubmitCheck ResubmitResult 596 615 } 597 616 598 617 func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error { ··· 627 646 } 628 647 629 648 type PullActionsParams struct { 630 - LoggedInUser *auth.User 631 - RepoInfo RepoInfo 632 - Pull *db.Pull 633 - RoundNumber int 634 - MergeCheck types.MergeCheckResponse 649 + LoggedInUser *auth.User 650 + RepoInfo RepoInfo 651 + Pull *db.Pull 652 + RoundNumber int 653 + MergeCheck types.MergeCheckResponse 654 + ResubmitCheck ResubmitResult 635 655 } 636 656 637 657 func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
+13 -1
appview/pages/templates/fragments/pullActions.html
··· 10 10 {{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} 11 11 {{ $isLastRound := eq $roundNumber $lastIdx }} 12 12 {{ $isSameRepoBranch := .Pull.IsSameRepoBranch }} 13 + {{ $isUpToDate := .ResubmitCheck.No }} 13 14 <div class="relative w-fit"> 14 15 <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 15 16 <div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2"> ··· 37 38 {{ end }} 38 39 39 40 {{ if and $isPullAuthor $isOpen $isLastRound }} 41 + {{ $disabled := "" }} 42 + {{ if $isUpToDate }} 43 + {{ $disabled = "disabled" }} 44 + {{ end }} 40 45 <button id="resubmitBtn" 41 46 {{ if $isSameRepoBranch }} 42 47 hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" ··· 47 52 {{ end }} 48 53 49 54 hx-disabled-elt="#resubmitBtn" 50 - class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"> 55 + class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" {{ $disabled }} 56 + 57 + {{ if $disabled }} 58 + title="Update this branch to resubmit this pull request" 59 + {{ else }} 60 + title="Resubmit this pull request" 61 + {{ end }} 62 + > 51 63 {{ i "rotate-ccw" "w-4 h-4" }} 52 64 <span>resubmit</span> 53 65 </button>
+14 -1
appview/pages/templates/repo/pulls/pull.html
··· 135 135 136 136 {{ if eq $lastIdx .RoundNumber }} 137 137 {{ block "mergeStatus" $ }} {{ end }} 138 + {{ block "resubmitStatus" $ }} {{ end }} 138 139 {{ end }} 139 140 140 141 {{ if $.LoggedInUser }} 141 - {{ template "fragments/pullActions" (dict "LoggedInUser" $.LoggedInUser "Pull" $.Pull "RepoInfo" $.RepoInfo "RoundNumber" .RoundNumber "MergeCheck" $.MergeCheck) }} 142 + {{ template "fragments/pullActions" (dict "LoggedInUser" $.LoggedInUser "Pull" $.Pull "RepoInfo" $.RepoInfo "RoundNumber" .RoundNumber "MergeCheck" $.MergeCheck "ResubmitCheck" $.ResubmitCheck) }} 142 143 {{ else }} 143 144 <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm px-6 py-4 w-fit dark:text-white"> 144 145 <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> ··· 209 210 </div> 210 211 {{ end }} 211 212 {{ end }} 213 + 214 + {{ define "resubmitStatus" }} 215 + {{ if .ResubmitCheck.Yes }} 216 + <div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm px-6 py-2 relative w-fit"> 217 + <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 218 + <div class="flex items-center gap-2 text-amber-500 dark:text-amber-300"> 219 + {{ i "triangle-alert" "w-4 h-4" }} 220 + <span class="font-medium">this branch has been updated, consider resubmitting</span> 221 + </div> 222 + </div> 223 + {{ end }} 224 + {{ end }}
+91 -28
appview/state/pull.go
··· 50 50 } 51 51 52 52 mergeCheckResponse := s.mergeCheck(f, pull) 53 + var resubmitResult pages.ResubmitResult 54 + if user.Did == pull.OwnerDid { 55 + resubmitResult = s.resubmitCheck(f, pull) 56 + } 53 57 54 58 s.pages.PullActionsFragment(w, pages.PullActionsParams{ 55 - LoggedInUser: user, 56 - RepoInfo: f.RepoInfo(s, user), 57 - Pull: pull, 58 - RoundNumber: roundNumber, 59 - MergeCheck: mergeCheckResponse, 59 + LoggedInUser: user, 60 + RepoInfo: f.RepoInfo(s, user), 61 + Pull: pull, 62 + RoundNumber: roundNumber, 63 + MergeCheck: mergeCheckResponse, 64 + ResubmitCheck: resubmitResult, 60 65 }) 61 66 return 62 67 } ··· 105 110 } 106 111 107 112 mergeCheckResponse := s.mergeCheck(f, pull) 113 + var resubmitResult pages.ResubmitResult 114 + if user.Did == pull.OwnerDid { 115 + resubmitResult = s.resubmitCheck(f, pull) 116 + } 108 117 109 118 s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{ 110 - LoggedInUser: user, 111 - RepoInfo: f.RepoInfo(s, user), 112 - DidHandleMap: didHandleMap, 113 - Pull: pull, 114 - MergeCheck: mergeCheckResponse, 119 + LoggedInUser: user, 120 + RepoInfo: f.RepoInfo(s, user), 121 + DidHandleMap: didHandleMap, 122 + Pull: pull, 123 + MergeCheck: mergeCheckResponse, 124 + ResubmitCheck: resubmitResult, 115 125 }) 116 126 } 117 127 ··· 175 185 return mergeCheckResponse 176 186 } 177 187 188 + func (s *State) resubmitCheck(f *FullyResolvedRepo, pull *db.Pull) pages.ResubmitResult { 189 + if pull.State == db.PullMerged { 190 + return pages.Unknown 191 + } 192 + 193 + if pull.PullSource == nil { 194 + return pages.Unknown 195 + } 196 + 197 + us, err := NewUnsignedClient(f.Knot, s.config.Dev) 198 + if err != nil { 199 + log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err) 200 + return pages.Unknown 201 + } 202 + 203 + resp, err := us.Branch(f.OwnerDid(), f.RepoName, pull.PullSource.Branch) 204 + if err != nil { 205 + log.Println("failed to reach knotserver", err) 206 + return pages.Unknown 207 + } 208 + 209 + body, err := io.ReadAll(resp.Body) 210 + if err != nil { 211 + log.Printf("Error reading response body: %v", err) 212 + return pages.Unknown 213 + } 214 + 215 + var result types.RepoBranchResponse 216 + err = json.Unmarshal(body, &result) 217 + if err != nil { 218 + log.Println("failed to parse response:", err) 219 + return pages.Unknown 220 + } 221 + 222 + if pull.Submissions[pull.LastRoundNumber()].SourceRev != result.Branch.Hash { 223 + log.Println(pull.Submissions[pull.LastRoundNumber()].SourceRev, result.Branch.Hash) 224 + return pages.ShouldResubmit 225 + } else { 226 + return pages.ShouldNotResubmit 227 + } 228 + } 229 + 178 230 func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) { 179 231 user := s.auth.GetUser(r) 180 232 f, err := fullyResolvedRepo(r) ··· 457 509 sourceBranch := r.FormValue("sourceBranch") 458 510 patch := r.FormValue("patch") 459 511 460 - if patch == "" { 461 - if isPushAllowed && sourceBranch == "" { 462 - s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.") 463 - return 464 - } 465 - s.pages.Notice(w, "pull", "Patch is empty.") 512 + isBranchBased := isPushAllowed && (sourceBranch != "") 513 + isPatchBased := patch != "" 514 + 515 + if !isBranchBased && !isPatchBased { 516 + s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.") 466 517 return 467 518 } 468 519 469 - if patch != "" && sourceBranch != "" { 520 + if isBranchBased && isPatchBased { 470 521 s.pages.Notice(w, "pull", "Cannot select both patch and source branch.") 471 522 return 472 523 } ··· 477 528 } 478 529 479 530 // TODO: check if knot has this capability 531 + var sourceRev string 480 532 var pullSource *db.PullSource 481 - if sourceBranch != "" && isPushAllowed { 533 + var recordPullSource *tangled.RepoPull_Source 534 + if isBranchBased { 482 535 pullSource = &db.PullSource{ 483 536 Branch: sourceBranch, 484 537 } 538 + recordPullSource = &tangled.RepoPull_Source{ 539 + Branch: sourceBranch, 540 + } 485 541 // generate a patch using /compare 486 542 ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 487 543 if err != nil { ··· 489 545 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 490 546 return 491 547 } 492 - 493 - log.Println(targetBranch, sourceBranch) 494 548 495 549 resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 496 550 switch resp.StatusCode { ··· 510 564 err = json.Unmarshal(respBody, &diffTreeResponse) 511 565 if err != nil { 512 566 log.Println("failed to unmarshal diff tree response", err) 513 - log.Println(string(respBody)) 514 567 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 515 568 } 516 569 570 + sourceRev = diffTreeResponse.DiffTree.Rev2 517 571 patch = diffTreeResponse.DiffTree.Patch 518 572 } 519 573 520 - log.Println(patch) 521 - 522 574 // Validate patch format 523 575 if !isPatchValid(patch) { 524 576 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") ··· 535 587 536 588 rkey := s.TID() 537 589 initialSubmission := db.PullSubmission{ 538 - Patch: patch, 590 + Patch: patch, 591 + SourceRev: sourceRev, 539 592 } 540 593 err = db.NewPull(tx, &db.Pull{ 541 594 Title: title, ··· 573 626 TargetRepo: string(f.RepoAt), 574 627 TargetBranch: targetBranch, 575 628 Patch: patch, 629 + Source: recordPullSource, 576 630 }, 577 631 }, 578 632 }) ··· 613 667 return 614 668 case http.MethodPost: 615 669 patch := r.FormValue("patch") 670 + var sourceRev string 671 + var recordPullSource *tangled.RepoPull_Source 616 672 617 673 // this pull is a branch based pull 618 674 isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed() 619 675 if pull.IsSameRepoBranch() && isPushAllowed { 620 676 sourceBranch := pull.PullSource.Branch 621 677 targetBranch := pull.TargetBranch 678 + recordPullSource = &tangled.RepoPull_Source{ 679 + Branch: sourceBranch, 680 + } 622 681 // extract patch by performing compare 623 682 ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 624 683 if err != nil { ··· 627 686 return 628 687 } 629 688 630 - log.Println(targetBranch, sourceBranch) 631 - 632 689 resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 633 690 switch resp.StatusCode { 634 691 case 404: ··· 647 704 err = json.Unmarshal(respBody, &diffTreeResponse) 648 705 if err != nil { 649 706 log.Println("failed to unmarshal diff tree response", err) 650 - log.Println(string(respBody)) 651 707 s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 652 708 } 653 709 710 + sourceRev = diffTreeResponse.DiffTree.Rev2 654 711 patch = diffTreeResponse.DiffTree.Patch 655 712 } 656 713 ··· 664 721 return 665 722 } 666 723 724 + if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev { 725 + s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.") 726 + return 727 + } 728 + 667 729 // Validate patch format 668 730 if !isPatchValid(patch) { 669 731 s.pages.Notice(w, "resubmit-error", "Invalid patch format. Please provide a valid diff.") ··· 678 740 } 679 741 defer tx.Rollback() 680 742 681 - err = db.ResubmitPull(tx, pull, patch) 743 + err = db.ResubmitPull(tx, pull, patch, sourceRev) 682 744 if err != nil { 683 745 log.Println("failed to create pull request", err) 684 746 s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.") ··· 705 767 TargetRepo: string(f.RepoAt), 706 768 TargetBranch: pull.TargetBranch, 707 769 Patch: patch, // new patch 770 + Source: recordPullSource, 708 771 }, 709 772 }, 710 773 })
+15
appview/state/signer.go
··· 287 287 return us.client.Do(req) 288 288 } 289 289 290 + func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) { 291 + const ( 292 + Method = "GET" 293 + ) 294 + 295 + endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, branch) 296 + 297 + req, err := us.newRequest(Method, endpoint, nil) 298 + if err != nil { 299 + return nil, err 300 + } 301 + 302 + return us.client.Do(req) 303 + } 304 + 290 305 func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*http.Response, error) { 291 306 const ( 292 307 Method = "GET"
-2
knotserver/git/diff.go
··· 97 97 return nil, fmt.Errorf("Invalid revision: %s", rev2) 98 98 } 99 99 100 - log.Println(commit1, commit2) 101 - 102 100 tree1, err := commit1.Tree() 103 101 if err != nil { 104 102 return nil, err
+13
knotserver/git/git.go
··· 238 238 return branches, nil 239 239 } 240 240 241 + func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) { 242 + ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false) 243 + if err != nil { 244 + return nil, fmt.Errorf("branch: %w", err) 245 + } 246 + 247 + if !ref.Name().IsBranch() { 248 + return nil, fmt.Errorf("branch: %s is not a branch", ref.Name()) 249 + } 250 + 251 + return ref, nil 252 + } 253 + 241 254 func (g *GitRepo) SetDefaultBranch(branch string) error { 242 255 ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch)) 243 256 return g.r.Storer.SetReference(ref)
+1
knotserver/handler.go
··· 106 106 r.Get("/tags", h.Tags) 107 107 r.Route("/branches", func(r chi.Router) { 108 108 r.Get("/", h.Branches) 109 + r.Get("/{branch}", h.Branch) 109 110 r.Route("/default", func(r chi.Router) { 110 111 r.Get("/", h.DefaultBranch) 111 112 r.With(h.VerifySignature).Put("/", h.SetDefaultBranch)
+31
knotserver/routes.go
··· 456 456 return 457 457 } 458 458 459 + func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) { 460 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 461 + branchName := chi.URLParam(r, "branch") 462 + l := h.l.With("handler", "Branch") 463 + 464 + gr, err := git.PlainOpen(path) 465 + if err != nil { 466 + notFound(w) 467 + return 468 + } 469 + 470 + ref, err := gr.Branch(branchName) 471 + if err != nil { 472 + l.Error("getting branches", "error", err.Error()) 473 + writeError(w, err.Error(), http.StatusInternalServerError) 474 + return 475 + } 476 + 477 + resp := types.RepoBranchResponse{ 478 + Branch: types.Branch{ 479 + Reference: types.Reference{ 480 + Name: ref.Name().Short(), 481 + Hash: ref.Hash().String(), 482 + }, 483 + }, 484 + } 485 + 486 + writeJSON(w, resp) 487 + return 488 + } 489 + 459 490 func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) { 460 491 l := h.l.With("handler", "Keys") 461 492
+4
types/repo.go
··· 67 67 Branches []Branch `json:"branches,omitempty"` 68 68 } 69 69 70 + type RepoBranchResponse struct { 71 + Branch Branch `json:"branch,omitempty"` 72 + } 73 + 70 74 type RepoDefaultBranchResponse struct { 71 75 Branch string `json:"branch,omitempty"` 72 76 }