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

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
···
return err
})
-
runMigration(db, "add-source-info-to-pulls", func(tx *sql.Tx) error {
+
runMigration(db, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
_, err := tx.Exec(`
alter table pulls add column source_branch text;
alter table pulls add column source_repo_at text;
+
alter table pull_submissions add column source_rev text;
`)
return err
})
+15 -8
appview/db/pulls.go
···
RoundNumber int
Patch string
Comments []PullComment
+
SourceRev string // include the rev that was used to create this submission: only for branch PRs
// meta
Created time.Time
···
}
_, err = tx.Exec(`
-
insert into pull_submissions (pull_id, repo_at, round_number, patch)
-
values (?, ?, ?, ?)
-
`, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch)
+
insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev)
+
values (?, ?, ?, ?, ?)
+
`, pull.PullId, pull.RepoAt, 0, pull.Submissions[0].Patch, pull.Submissions[0].SourceRev)
if err != nil {
return err
}
···
submissionsQuery := `
select
-
id, pull_id, repo_at, round_number, patch, created
+
id, pull_id, repo_at, round_number, patch, created, source_rev
from
pull_submissions
where
···
for submissionsRows.Next() {
var submission PullSubmission
var submissionCreatedStr string
+
var submissionSourceRev sql.NullString
err := submissionsRows.Scan(
&submission.ID,
&submission.PullId,
···
&submission.RoundNumber,
&submission.Patch,
&submissionCreatedStr,
+
&submissionSourceRev,
)
if err != nil {
return nil, err
···
}
submission.Created = submissionCreatedTime
+
if submissionSourceRev.Valid {
+
submission.SourceRev = submissionSourceRev.String
+
}
+
submissionsMap[submission.ID] = &submission
}
if err = submissionsRows.Close(); err != nil {
···
return err
}
-
func ResubmitPull(e Execer, pull *Pull, newPatch string) error {
+
func ResubmitPull(e Execer, pull *Pull, newPatch, sourceRev string) error {
newRoundNumber := len(pull.Submissions)
_, err := e.Exec(`
-
insert into pull_submissions (pull_id, repo_at, round_number, patch)
-
values (?, ?, ?, ?)
-
`, pull.PullId, pull.RepoAt, newRoundNumber, newPatch)
+
insert into pull_submissions (pull_id, repo_at, round_number, patch, source_rev)
+
values (?, ?, ?, ?, ?)
+
`, pull.PullId, pull.RepoAt, newRoundNumber, newPatch, sourceRev)
return err
}
+31 -11
appview/pages/pages.go
···
return p.executeRepo("repo/pulls/pulls", w, params)
}
+
type ResubmitResult uint64
+
+
const (
+
ShouldResubmit ResubmitResult = iota
+
ShouldNotResubmit
+
Unknown
+
)
+
+
func (r ResubmitResult) Yes() bool {
+
return r == ShouldResubmit
+
}
+
func (r ResubmitResult) No() bool {
+
return r == ShouldNotResubmit
+
}
+
func (r ResubmitResult) Unknown() bool {
+
return r == Unknown
+
}
+
type RepoSinglePullParams struct {
-
LoggedInUser *auth.User
-
RepoInfo RepoInfo
-
Active string
-
DidHandleMap map[string]string
-
Pull *db.Pull
-
MergeCheck types.MergeCheckResponse
+
LoggedInUser *auth.User
+
RepoInfo RepoInfo
+
Active string
+
DidHandleMap map[string]string
+
Pull *db.Pull
+
MergeCheck types.MergeCheckResponse
+
ResubmitCheck ResubmitResult
}
func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
···
}
type PullActionsParams struct {
-
LoggedInUser *auth.User
-
RepoInfo RepoInfo
-
Pull *db.Pull
-
RoundNumber int
-
MergeCheck types.MergeCheckResponse
+
LoggedInUser *auth.User
+
RepoInfo RepoInfo
+
Pull *db.Pull
+
RoundNumber int
+
MergeCheck types.MergeCheckResponse
+
ResubmitCheck ResubmitResult
}
func (p *Pages) PullActionsFragment(w io.Writer, params PullActionsParams) error {
+13 -1
appview/pages/templates/fragments/pullActions.html
···
{{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }}
{{ $isLastRound := eq $roundNumber $lastIdx }}
{{ $isSameRepoBranch := .Pull.IsSameRepoBranch }}
+
{{ $isUpToDate := .ResubmitCheck.No }}
<div class="relative w-fit">
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
<div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2">
···
{{ end }}
{{ if and $isPullAuthor $isOpen $isLastRound }}
+
{{ $disabled := "" }}
+
{{ if $isUpToDate }}
+
{{ $disabled = "disabled" }}
+
{{ end }}
<button id="resubmitBtn"
{{ if $isSameRepoBranch }}
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
···
{{ end }}
hx-disabled-elt="#resubmitBtn"
-
class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
+
class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed" {{ $disabled }}
+
+
{{ if $disabled }}
+
title="Update this branch to resubmit this pull request"
+
{{ else }}
+
title="Resubmit this pull request"
+
{{ end }}
+
>
{{ i "rotate-ccw" "w-4 h-4" }}
<span>resubmit</span>
</button>
+14 -1
appview/pages/templates/repo/pulls/pull.html
···
{{ if eq $lastIdx .RoundNumber }}
{{ block "mergeStatus" $ }} {{ end }}
+
{{ block "resubmitStatus" $ }} {{ end }}
{{ end }}
{{ if $.LoggedInUser }}
-
{{ template "fragments/pullActions" (dict "LoggedInUser" $.LoggedInUser "Pull" $.Pull "RepoInfo" $.RepoInfo "RoundNumber" .RoundNumber "MergeCheck" $.MergeCheck) }}
+
{{ template "fragments/pullActions" (dict "LoggedInUser" $.LoggedInUser "Pull" $.Pull "RepoInfo" $.RepoInfo "RoundNumber" .RoundNumber "MergeCheck" $.MergeCheck "ResubmitCheck" $.ResubmitCheck) }}
{{ else }}
<div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm px-6 py-4 w-fit dark:text-white">
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
···
</div>
{{ end }}
{{ end }}
+
+
{{ define "resubmitStatus" }}
+
{{ if .ResubmitCheck.Yes }}
+
<div class="bg-amber-50 dark:bg-amber-900 border border-amber-500 rounded drop-shadow-sm px-6 py-2 relative w-fit">
+
<div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div>
+
<div class="flex items-center gap-2 text-amber-500 dark:text-amber-300">
+
{{ i "triangle-alert" "w-4 h-4" }}
+
<span class="font-medium">this branch has been updated, consider resubmitting</span>
+
</div>
+
</div>
+
{{ end }}
+
{{ end }}
+91 -28
appview/state/pull.go
···
}
mergeCheckResponse := s.mergeCheck(f, pull)
+
var resubmitResult pages.ResubmitResult
+
if user.Did == pull.OwnerDid {
+
resubmitResult = s.resubmitCheck(f, pull)
+
}
s.pages.PullActionsFragment(w, pages.PullActionsParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
-
Pull: pull,
-
RoundNumber: roundNumber,
-
MergeCheck: mergeCheckResponse,
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
+
Pull: pull,
+
RoundNumber: roundNumber,
+
MergeCheck: mergeCheckResponse,
+
ResubmitCheck: resubmitResult,
})
return
}
···
}
mergeCheckResponse := s.mergeCheck(f, pull)
+
var resubmitResult pages.ResubmitResult
+
if user.Did == pull.OwnerDid {
+
resubmitResult = s.resubmitCheck(f, pull)
+
}
s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
-
LoggedInUser: user,
-
RepoInfo: f.RepoInfo(s, user),
-
DidHandleMap: didHandleMap,
-
Pull: pull,
-
MergeCheck: mergeCheckResponse,
+
LoggedInUser: user,
+
RepoInfo: f.RepoInfo(s, user),
+
DidHandleMap: didHandleMap,
+
Pull: pull,
+
MergeCheck: mergeCheckResponse,
+
ResubmitCheck: resubmitResult,
})
}
···
return mergeCheckResponse
}
+
func (s *State) resubmitCheck(f *FullyResolvedRepo, pull *db.Pull) pages.ResubmitResult {
+
if pull.State == db.PullMerged {
+
return pages.Unknown
+
}
+
+
if pull.PullSource == nil {
+
return pages.Unknown
+
}
+
+
us, err := NewUnsignedClient(f.Knot, s.config.Dev)
+
if err != nil {
+
log.Printf("failed to setup signed client for %s; ignoring: %v", f.Knot, err)
+
return pages.Unknown
+
}
+
+
resp, err := us.Branch(f.OwnerDid(), f.RepoName, pull.PullSource.Branch)
+
if err != nil {
+
log.Println("failed to reach knotserver", err)
+
return pages.Unknown
+
}
+
+
body, err := io.ReadAll(resp.Body)
+
if err != nil {
+
log.Printf("Error reading response body: %v", err)
+
return pages.Unknown
+
}
+
+
var result types.RepoBranchResponse
+
err = json.Unmarshal(body, &result)
+
if err != nil {
+
log.Println("failed to parse response:", err)
+
return pages.Unknown
+
}
+
+
if pull.Submissions[pull.LastRoundNumber()].SourceRev != result.Branch.Hash {
+
log.Println(pull.Submissions[pull.LastRoundNumber()].SourceRev, result.Branch.Hash)
+
return pages.ShouldResubmit
+
} else {
+
return pages.ShouldNotResubmit
+
}
+
}
+
func (s *State) RepoPullPatch(w http.ResponseWriter, r *http.Request) {
user := s.auth.GetUser(r)
f, err := fullyResolvedRepo(r)
···
sourceBranch := r.FormValue("sourceBranch")
patch := r.FormValue("patch")
-
if patch == "" {
-
if isPushAllowed && sourceBranch == "" {
-
s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.")
-
return
-
}
-
s.pages.Notice(w, "pull", "Patch is empty.")
+
isBranchBased := isPushAllowed && (sourceBranch != "")
+
isPatchBased := patch != ""
+
+
if !isBranchBased && !isPatchBased {
+
s.pages.Notice(w, "pull", "Neither source branch nor patch supplied.")
return
}
-
if patch != "" && sourceBranch != "" {
+
if isBranchBased && isPatchBased {
s.pages.Notice(w, "pull", "Cannot select both patch and source branch.")
return
}
···
}
// TODO: check if knot has this capability
+
var sourceRev string
var pullSource *db.PullSource
-
if sourceBranch != "" && isPushAllowed {
+
var recordPullSource *tangled.RepoPull_Source
+
if isBranchBased {
pullSource = &db.PullSource{
Branch: sourceBranch,
}
+
recordPullSource = &tangled.RepoPull_Source{
+
Branch: sourceBranch,
+
}
// generate a patch using /compare
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
···
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
return
}
-
-
log.Println(targetBranch, sourceBranch)
resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch)
switch resp.StatusCode {
···
err = json.Unmarshal(respBody, &diffTreeResponse)
if err != nil {
log.Println("failed to unmarshal diff tree response", err)
-
log.Println(string(respBody))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
}
+
sourceRev = diffTreeResponse.DiffTree.Rev2
patch = diffTreeResponse.DiffTree.Patch
}
-
log.Println(patch)
-
// Validate patch format
if !isPatchValid(patch) {
s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.")
···
rkey := s.TID()
initialSubmission := db.PullSubmission{
-
Patch: patch,
+
Patch: patch,
+
SourceRev: sourceRev,
}
err = db.NewPull(tx, &db.Pull{
Title: title,
···
TargetRepo: string(f.RepoAt),
TargetBranch: targetBranch,
Patch: patch,
+
Source: recordPullSource,
},
},
})
···
return
case http.MethodPost:
patch := r.FormValue("patch")
+
var sourceRev string
+
var recordPullSource *tangled.RepoPull_Source
// this pull is a branch based pull
isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed()
if pull.IsSameRepoBranch() && isPushAllowed {
sourceBranch := pull.PullSource.Branch
targetBranch := pull.TargetBranch
+
recordPullSource = &tangled.RepoPull_Source{
+
Branch: sourceBranch,
+
}
// extract patch by performing compare
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
if err != nil {
···
return
}
-
log.Println(targetBranch, sourceBranch)
-
resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch)
switch resp.StatusCode {
case 404:
···
err = json.Unmarshal(respBody, &diffTreeResponse)
if err != nil {
log.Println("failed to unmarshal diff tree response", err)
-
log.Println(string(respBody))
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
}
+
sourceRev = diffTreeResponse.DiffTree.Rev2
patch = diffTreeResponse.DiffTree.Patch
}
···
return
}
+
if sourceRev == pull.Submissions[pull.LastRoundNumber()].SourceRev {
+
s.pages.Notice(w, "resubmit-error", "This branch has not changed since the last submission.")
+
return
+
}
+
// Validate patch format
if !isPatchValid(patch) {
s.pages.Notice(w, "resubmit-error", "Invalid patch format. Please provide a valid diff.")
···
}
defer tx.Rollback()
-
err = db.ResubmitPull(tx, pull, patch)
+
err = db.ResubmitPull(tx, pull, patch, sourceRev)
if err != nil {
log.Println("failed to create pull request", err)
s.pages.Notice(w, "resubmit-error", "Failed to create pull request. Try again later.")
···
TargetRepo: string(f.RepoAt),
TargetBranch: pull.TargetBranch,
Patch: patch, // new patch
+
Source: recordPullSource,
},
},
})
+15
appview/state/signer.go
···
return us.client.Do(req)
}
+
func (us *UnsignedClient) Branch(ownerDid, repoName, branch string) (*http.Response, error) {
+
const (
+
Method = "GET"
+
)
+
+
endpoint := fmt.Sprintf("/%s/%s/branches/%s", ownerDid, repoName, branch)
+
+
req, err := us.newRequest(Method, endpoint, nil)
+
if err != nil {
+
return nil, err
+
}
+
+
return us.client.Do(req)
+
}
+
func (us *UnsignedClient) DefaultBranch(ownerDid, repoName string) (*http.Response, error) {
const (
Method = "GET"
-2
knotserver/git/diff.go
···
return nil, fmt.Errorf("Invalid revision: %s", rev2)
}
-
log.Println(commit1, commit2)
-
tree1, err := commit1.Tree()
if err != nil {
return nil, err
+13
knotserver/git/git.go
···
return branches, nil
}
+
func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) {
+
ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false)
+
if err != nil {
+
return nil, fmt.Errorf("branch: %w", err)
+
}
+
+
if !ref.Name().IsBranch() {
+
return nil, fmt.Errorf("branch: %s is not a branch", ref.Name())
+
}
+
+
return ref, nil
+
}
+
func (g *GitRepo) SetDefaultBranch(branch string) error {
ref := plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName(branch))
return g.r.Storer.SetReference(ref)
+1
knotserver/handler.go
···
r.Get("/tags", h.Tags)
r.Route("/branches", func(r chi.Router) {
r.Get("/", h.Branches)
+
r.Get("/{branch}", h.Branch)
r.Route("/default", func(r chi.Router) {
r.Get("/", h.DefaultBranch)
r.With(h.VerifySignature).Put("/", h.SetDefaultBranch)
+31
knotserver/routes.go
···
return
}
+
func (h *Handle) Branch(w http.ResponseWriter, r *http.Request) {
+
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
+
branchName := chi.URLParam(r, "branch")
+
l := h.l.With("handler", "Branch")
+
+
gr, err := git.PlainOpen(path)
+
if err != nil {
+
notFound(w)
+
return
+
}
+
+
ref, err := gr.Branch(branchName)
+
if err != nil {
+
l.Error("getting branches", "error", err.Error())
+
writeError(w, err.Error(), http.StatusInternalServerError)
+
return
+
}
+
+
resp := types.RepoBranchResponse{
+
Branch: types.Branch{
+
Reference: types.Reference{
+
Name: ref.Name().Short(),
+
Hash: ref.Hash().String(),
+
},
+
},
+
}
+
+
writeJSON(w, resp)
+
return
+
}
+
func (h *Handle) Keys(w http.ResponseWriter, r *http.Request) {
l := h.l.With("handler", "Keys")
+4
types/repo.go
···
Branches []Branch `json:"branches,omitempty"`
}
+
type RepoBranchResponse struct {
+
Branch Branch `json:"branch,omitempty"`
+
}
+
type RepoDefaultBranchResponse struct {
Branch string `json:"branch,omitempty"`
}