+2
-1
appview/db/db.go
+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
+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
+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
+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
+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
+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
+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
-2
knotserver/git/diff.go
+13
knotserver/git/git.go
+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
+1
knotserver/handler.go
+31
knotserver/routes.go
+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