diff --git a/appview/db/db.go b/appview/db/db.go
index 00182c46..605798c3 100644
--- a/appview/db/db.go
+++ b/appview/db/db.go
@@ -109,8 +109,11 @@ func Make(dbPath string) (*DB, error) {
repo_at text not null,
pull_id integer not null,
title text not null,
+ body text not null,
patch text,
- patch_at text not null,
+ pull_at text,
+ rkey text not null,
+ target_branch text not null,
open integer not null default 1,
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
unique(repo_at, pull_id),
diff --git a/appview/db/pulls.go b/appview/db/pulls.go
index 92233efa..bf9db112 100644
--- a/appview/db/pulls.go
+++ b/appview/db/pulls.go
@@ -7,30 +7,33 @@ import (
"github.com/bluesky-social/indigo/atproto/syntax"
)
-type Pulls struct {
- ID int `json:"id"`
- OwnerDid string `json:"owner_did"`
- RepoAt string `json:"repo_at"`
- PullId int `json:"pull_id"`
- Title string `json:"title"`
- Patch string `json:"patch,omitempty"`
- PatchAt string `json:"patch_at"`
- Open int `json:"open"`
- Created time.Time `json:"created"`
-}
-
-type PullComments struct {
- ID int `json:"id"`
- OwnerDid string `json:"owner_did"`
- PullId int `json:"pull_id"`
- RepoAt string `json:"repo_at"`
- CommentId int `json:"comment_id"`
- CommentAt string `json:"comment_at"`
- Body string `json:"body"`
- Created time.Time `json:"created"`
-}
-
-func NewPull(tx *sql.Tx, pull *Pulls) error {
+type Pull struct {
+ ID int
+ OwnerDid string
+ RepoAt syntax.ATURI
+ PullAt syntax.ATURI
+ TargetBranch string
+ Patch string
+ PullId int
+ Title string
+ Body string
+ Open int
+ Created time.Time
+ Rkey string
+}
+
+type PullComment struct {
+ ID int
+ OwnerDid string
+ PullId int
+ RepoAt string
+ CommentId int
+ CommentAt string
+ Body string
+ Created time.Time
+}
+
+func NewPull(tx *sql.Tx, pull *Pull) error {
defer tx.Rollback()
_, err := tx.Exec(`
@@ -55,9 +58,9 @@ func NewPull(tx *sql.Tx, pull *Pulls) error {
pull.PullId = nextId
_, err = tx.Exec(`
- insert into pulls (repo_at, owner_did, pull_id, title, patch)
- values (?, ?, ?, ?, ?)
- `, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.Patch)
+ insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, patch, rkey)
+ values (?, ?, ?, ?, ?, ?, ?, ?)
+ `, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.TargetBranch, pull.Body, pull.Patch, pull.Rkey)
if err != nil {
return err
}
@@ -70,13 +73,13 @@ func NewPull(tx *sql.Tx, pull *Pulls) error {
}
func SetPullAt(e Execer, repoAt syntax.ATURI, pullId int, pullAt string) error {
- _, err := e.Exec(`update pulls set patch_at = ? where repo_at = ? and pull_id = ?`, pullAt, repoAt, pullId)
+ _, err := e.Exec(`update pulls set pull_at = ? where repo_at = ? and pull_id = ?`, pullAt, repoAt, pullId)
return err
}
func GetPullAt(e Execer, repoAt syntax.ATURI, pullId int) (string, error) {
var pullAt string
- err := e.QueryRow(`select patch_at from pulls where repo_at = ? and pull_id = ?`, repoAt, pullId).Scan(&pullAt)
+ err := e.QueryRow(`select pull_at from pulls where repo_at = ? and pull_id = ?`, repoAt, pullId).Scan(&pullAt)
return pullAt, err
}
@@ -92,19 +95,19 @@ func GetPullOwnerDid(e Execer, repoAt syntax.ATURI, pullId int) (string, error)
return ownerDid, err
}
-func GetPulls(e Execer, repoAt syntax.ATURI) ([]Pulls, error) {
- var pulls []Pulls
+func GetPulls(e Execer, repoAt syntax.ATURI) ([]Pull, error) {
+ var pulls []Pull
- rows, err := e.Query(`select owner_did, pull_id, created, title, patch, open from pulls where repo_at = ? order by created desc`, repoAt)
+ rows, err := e.Query(`select owner_did, pull_id, created, title, open, target_branch, pull_at, body, patch, rkey from pulls where repo_at = ? order by created desc`, repoAt)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
- var pull Pulls
+ var pull Pull
var createdAt string
- err := rows.Scan(&pull.OwnerDid, &pull.PullId, &createdAt, &pull.Title, &pull.Patch, &pull.Open)
+ err := rows.Scan(&pull.OwnerDid, &pull.PullId, &createdAt, &pull.Title, &pull.Open, &pull.TargetBranch, &pull.PullAt, &pull.Body, &pull.Patch, &pull.Rkey)
if err != nil {
return nil, err
}
@@ -125,13 +128,13 @@ func GetPulls(e Execer, repoAt syntax.ATURI) ([]Pulls, error) {
return pulls, nil
}
-func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pulls, error) {
- query := `select owner_did, created, title, patch, open from pulls where repo_at = ? and pull_id = ?`
+func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, error) {
+ query := `select owner_did, created, title, open, target_branch, pull_at, body, patch, rkey from pulls where repo_at = ? and pull_id = ?`
row := e.QueryRow(query, repoAt, pullId)
- var pull Pulls
+ var pull Pull
var createdAt string
- err := row.Scan(&pull.OwnerDid, &createdAt, &pull.Title, &pull.Patch, &pull.Open)
+ err := row.Scan(&pull.OwnerDid, &createdAt, &pull.Title, &pull.Open, &pull.TargetBranch, &pull.PullAt, &pull.Body, &pull.Patch, &pull.Rkey)
if err != nil {
return nil, err
}
@@ -145,13 +148,13 @@ func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*Pulls, error) {
return &pull, nil
}
-func GetPullWithComments(e Execer, repoAt syntax.ATURI, pullId int) (*Pulls, []PullComments, error) {
- query := `select owner_did, pull_id, created, title, patch, open from pulls where repo_at = ? and pull_id = ?`
+func GetPullWithComments(e Execer, repoAt syntax.ATURI, pullId int) (*Pull, []PullComment, error) {
+ query := `select owner_did, pull_id, created, title, open, target_branch, pull_at, body, patch, rkey from pulls where repo_at = ? and pull_id = ?`
row := e.QueryRow(query, repoAt, pullId)
- var pull Pulls
+ var pull Pull
var createdAt string
- err := row.Scan(&pull.OwnerDid, &pull.PullId, &createdAt, &pull.Title, &pull.Patch, &pull.Open)
+ err := row.Scan(&pull.OwnerDid, &pull.PullId, &createdAt, &pull.Title, &pull.Open, &pull.TargetBranch, &pull.PullAt, &pull.Body, &pull.Patch, &pull.Rkey)
if err != nil {
return nil, nil, err
}
@@ -170,7 +173,7 @@ func GetPullWithComments(e Execer, repoAt syntax.ATURI, pullId int) (*Pulls, []P
return &pull, comments, nil
}
-func NewPullComment(e Execer, comment *PullComments) error {
+func NewPullComment(e Execer, comment *PullComment) error {
query := `insert into pull_comments (owner_did, repo_at, comment_at, pull_id, comment_id, body) values (?, ?, ?, ?, ?, ?)`
_, err := e.Exec(
query,
@@ -184,12 +187,12 @@ func NewPullComment(e Execer, comment *PullComments) error {
return err
}
-func GetPullComments(e Execer, repoAt syntax.ATURI, pullId int) ([]PullComments, error) {
- var comments []PullComments
+func GetPullComments(e Execer, repoAt syntax.ATURI, pullId int) ([]PullComment, error) {
+ var comments []PullComment
rows, err := e.Query(`select owner_did, pull_id, comment_id, comment_at, body, created from pull_comments where repo_at = ? and pull_id = ? order by created asc`, repoAt, pullId)
if err == sql.ErrNoRows {
- return []PullComments{}, nil
+ return []PullComment{}, nil
}
if err != nil {
return nil, err
@@ -197,7 +200,7 @@ func GetPullComments(e Execer, repoAt syntax.ATURI, pullId int) ([]PullComments,
defer rows.Close()
for rows.Next() {
- var comment PullComments
+ var comment PullComment
var createdAt string
err := rows.Scan(&comment.OwnerDid, &comment.PullId, &comment.CommentId, &comment.CommentAt, &comment.Body, &createdAt)
if err != nil {
diff --git a/appview/pages/pages.go b/appview/pages/pages.go
index 010c4aa0..d0eb8d81 100644
--- a/appview/pages/pages.go
+++ b/appview/pages/pages.go
@@ -506,10 +506,23 @@ func (p *Pages) RepoNewIssue(w io.Writer, params RepoNewIssueParams) error {
return p.executeRepo("repo/issues/new", w, params)
}
+type RepoNewPullParams struct {
+ LoggedInUser *auth.User
+ RepoInfo RepoInfo
+ Active string
+}
+
+func (p *Pages) RepoNewPull(w io.Writer, params RepoNewPullParams) error {
+ params.Active = "pulls"
+ return p.executeRepo("repo/pulls/new", w, params)
+}
+
type RepoPullsParams struct {
LoggedInUser *auth.User
RepoInfo RepoInfo
+ Pulls []db.Pull
Active string
+ DidHandleMap map[string]string
}
func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error {
@@ -517,6 +530,20 @@ func (p *Pages) RepoPulls(w io.Writer, params RepoPullsParams) error {
return p.executeRepo("repo/pulls/pulls", w, params)
}
+type RepoSinglePullParams struct {
+ LoggedInUser *auth.User
+ RepoInfo RepoInfo
+ DidHandleMap map[string]string
+ Pull db.Pull
+ Comments []db.PullComment
+ Active string
+}
+
+func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
+ params.Active = "pulls"
+ return p.executeRepo("repo/pulls/pull", w, params)
+}
+
func (p *Pages) Static() http.Handler {
sub, err := fs.Sub(files, "static")
if err != nil {
diff --git a/appview/pages/templates/repo/pulls/new.html b/appview/pages/templates/repo/pulls/new.html
new file mode 100644
index 00000000..f7884cc1
--- /dev/null
+++ b/appview/pages/templates/repo/pulls/new.html
@@ -0,0 +1,38 @@
+{{ define "title" }}new pull | {{ .RepoInfo.FullName }}{{ end }}
+
+{{ define "repoContent" }}
+
+{{ end }}
diff --git a/appview/pages/templates/repo/pulls/pull.html b/appview/pages/templates/repo/pulls/pull.html
new file mode 100644
index 00000000..bd30054d
--- /dev/null
+++ b/appview/pages/templates/repo/pulls/pull.html
@@ -0,0 +1,133 @@
+{{ define "title" }}
+ {{ .Pull.Title }} ·
+ {{ .RepoInfo.FullName }}
+{{ end }}
+
+{{ define "repoContent" }}
+
+ {{ .Pull.Title }}
+ #{{ .Pull.PullId }}
+
+
+ {{ $bgColor := "bg-gray-800" }}
+ {{ $icon := "ban" }}
+ {{ if eq .State "open" }}
+ {{ $bgColor = "bg-green-600" }}
+ {{ $icon = "circle-dot" }}
+ {{ end }}
+
+
+
+
+
+
+ {{ .State }}
+
+
+ opened by
+ {{ $owner := didOrHandle .Pull.OwnerDid .PullOwnerHandle }}
+ {{ $owner }}
+
+
+
+
+
+ {{ if .Pull.Body }}
+
+ {{ .Pull.Body | markdown }}
+
+ {{ end }}
+
+{{ end }}
+
+{{ define "repoAfter" }}
+
+
+ {{ if .LoggedInUser }}
+
+ {{ end }}
+
+ {{ if eq .LoggedInUser.Did .Pull.OwnerDid }}
+ {{ $action := "close" }}
+ {{ $icon := "circle-x" }}
+ {{ $hoverColor := "red" }}
+ {{ if eq .State "closed" }}
+ {{ $action = "reopen" }}
+ {{ $icon = "circle-dot" }}
+ {{ $hoverColor = "green" }}
+ {{ end }}
+
+ {{ end }}
+{{ end }}
diff --git a/appview/state/repo.go b/appview/state/repo.go
index 0553c8df..57c29e92 100644
--- a/appview/state/repo.go
+++ b/appview/state/repo.go
@@ -234,6 +234,200 @@ func (s *State) RepoDescription(w http.ResponseWriter, r *http.Request) {
}
}
+// MergeCheck gets called async, every time the patch diff is updated in a pull.
+func (s *State) MergeCheck(w http.ResponseWriter, r *http.Request) {
+ user := s.auth.GetUser(r)
+ f, err := fullyResolvedRepo(r)
+ if err != nil {
+ log.Println("failed to get repo and knot", err)
+ s.pages.Notice(w, "pull", "Failed to check mergeability. Try again later.")
+ return
+ }
+
+ patch := r.FormValue("patch")
+ targetBranch := r.FormValue("targetBranch")
+
+ if patch == "" || targetBranch == "" {
+ s.pages.Notice(w, "pull", "Patch and target branch are required.")
+ return
+ }
+
+ secret, err := db.GetRegistrationKey(s.db, f.Knot)
+ if err != nil {
+ log.Printf("no key found for domain %s: %s\n", f.Knot, err)
+ s.pages.Notice(w, "pull", "Failed to check mergeability. Try again later.")
+ return
+ }
+
+ ksClient, err := NewSignedClient(f.Knot, secret, s.config.Dev)
+ if err != nil {
+ log.Printf("failed to create signed client for %s", f.Knot)
+ s.pages.Notice(w, "pull", "Failed to check mergeability. Try again later.")
+ return
+ }
+
+ resp, err := ksClient.MergeCheck([]byte(patch), user.Did, f.RepoName, targetBranch)
+ if err != nil {
+ log.Println("failed to check mergeability", err)
+ s.pages.Notice(w, "pull", "Unable to check for mergeability. Try again later.")
+ return
+ }
+
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Println("failed to read knotserver response body")
+ s.pages.Notice(w, "pull", "Unable to check for mergeability. Try again later.")
+ return
+ }
+
+ var mergeCheckResponse types.MergeCheckResponse
+ err = json.Unmarshal(respBody, &mergeCheckResponse)
+ if err != nil {
+ log.Println("failed to unmarshal merge check response", err)
+ s.pages.Notice(w, "pull", "Failed to check mergeability. Try again later.")
+ return
+ }
+
+ // TODO: this has to return a html fragment
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(mergeCheckResponse)
+}
+
+func (s *State) NewPull(w http.ResponseWriter, r *http.Request) {
+ user := s.auth.GetUser(r)
+ f, err := fullyResolvedRepo(r)
+ if err != nil {
+ log.Println("failed to get repo and knot", err)
+ return
+ }
+
+ switch r.Method {
+ case http.MethodGet:
+ s.pages.RepoNewPull(w, pages.RepoNewPullParams{
+ LoggedInUser: user,
+ RepoInfo: f.RepoInfo(s, user),
+ })
+ case http.MethodPost:
+ title := r.FormValue("title")
+ body := r.FormValue("body")
+ targetBranch := r.FormValue("targetBranch")
+ patch := r.FormValue("patch")
+
+ if title == "" || body == "" || patch == "" {
+ s.pages.Notice(w, "pull", "Title, body and patch diff are required.")
+ return
+ }
+
+ tx, err := s.db.BeginTx(r.Context(), nil)
+ if err != nil {
+ log.Println("failed to start tx")
+ s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
+ return
+ }
+
+ defer func() {
+ tx.Rollback()
+ err = s.enforcer.E.LoadPolicy()
+ if err != nil {
+ log.Println("failed to rollback policies")
+ }
+ }()
+
+ err = db.NewPull(tx, &db.Pull{
+ Title: title,
+ Body: body,
+ TargetBranch: targetBranch,
+ Patch: patch,
+ OwnerDid: user.Did,
+ RepoAt: f.RepoAt,
+ })
+ if err != nil {
+ log.Println("failed to create pull request", err)
+ s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
+ return
+ }
+ client, _ := s.auth.AuthorizedClient(r)
+ pullId, err := db.GetPullId(s.db, f.RepoAt)
+ if err != nil {
+ log.Println("failed to get pull id", err)
+ s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
+ return
+ }
+
+ atResp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
+ Collection: tangled.RepoPullNSID,
+ Repo: user.Did,
+ Rkey: s.TID(),
+ Record: &lexutil.LexiconTypeDecoder{
+ Val: &tangled.RepoPull{
+ Title: title,
+ PullId: int64(pullId),
+ TargetRepo: string(f.RepoAt),
+ TargetBranch: targetBranch,
+ Patch: patch,
+ },
+ },
+ })
+
+ err = db.SetPullAt(s.db, f.RepoAt, pullId, atResp.Uri)
+ if err != nil {
+ log.Println("failed to get pull id", err)
+ s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
+ return
+ }
+
+ s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d", f.OwnerSlashRepo(), pullId))
+ return
+ }
+}
+
+func (s *State) RepoSinglePull(w http.ResponseWriter, r *http.Request) {
+ user := s.auth.GetUser(r)
+ f, err := fullyResolvedRepo(r)
+ if err != nil {
+ log.Println("failed to get repo and knot", err)
+ return
+ }
+
+ prId := chi.URLParam(r, "pull")
+ prIdInt, err := strconv.Atoi(prId)
+ if err != nil {
+ http.Error(w, "bad pr id", http.StatusBadRequest)
+ log.Println("failed to parse pr id", err)
+ return
+ }
+
+ pr, comments, err := db.GetPullWithComments(s.db, f.RepoAt, prIdInt)
+ if err != nil {
+ log.Println("failed to get pr and comments", err)
+ s.pages.Notice(w, "pull", "Failed to load pull request. Try again later.")
+ return
+ }
+
+ identsToResolve := make([]string, len(comments))
+ for i, comment := range comments {
+ identsToResolve[i] = comment.OwnerDid
+ }
+ resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
+ didHandleMap := make(map[string]string)
+ for _, identity := range resolvedIds {
+ if !identity.Handle.IsInvalidHandle() {
+ didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
+ } else {
+ didHandleMap[identity.DID.String()] = identity.DID.String()
+ }
+ }
+
+ s.pages.RepoSinglePull(w, pages.RepoSinglePullParams{
+ LoggedInUser: user,
+ RepoInfo: f.RepoInfo(s, user),
+ Pull: *pr,
+ Comments: comments,
+
+ DidHandleMap: didHandleMap,
+ })
+}
+
func (s *State) RepoCommit(w http.ResponseWriter, r *http.Request) {
f, err := fullyResolvedRepo(r)
if err != nil {
@@ -1071,13 +1265,34 @@ func (s *State) RepoPulls(w http.ResponseWriter, r *http.Request) {
return
}
- switch r.Method {
- case http.MethodGet:
- s.pages.RepoPulls(w, pages.RepoPullsParams{
- LoggedInUser: user,
- RepoInfo: f.RepoInfo(s, user),
- })
+ pulls, err := db.GetPulls(s.db, f.RepoAt)
+ if err != nil {
+ log.Println("failed to get pulls", err)
+ s.pages.Notice(w, "pulls", "Failed to load pulls. Try again later.")
+ return
}
+
+ identsToResolve := make([]string, len(pulls))
+ for i, pull := range pulls {
+ identsToResolve[i] = pull.OwnerDid
+ }
+ resolvedIds := s.resolver.ResolveIdents(r.Context(), identsToResolve)
+ didHandleMap := make(map[string]string)
+ for _, identity := range resolvedIds {
+ if !identity.Handle.IsInvalidHandle() {
+ didHandleMap[identity.DID.String()] = fmt.Sprintf("@%s", identity.Handle.String())
+ } else {
+ didHandleMap[identity.DID.String()] = identity.DID.String()
+ }
+ }
+
+ s.pages.RepoPulls(w, pages.RepoPullsParams{
+ LoggedInUser: s.auth.GetUser(r),
+ RepoInfo: f.RepoInfo(s, user),
+ Pulls: pulls,
+ DidHandleMap: didHandleMap,
+ })
+ return
}
func fullyResolvedRepo(r *http.Request) (*FullyResolvedRepo, error) {
diff --git a/appview/state/signer.go b/appview/state/signer.go
index 5b9e2c4b..35702ddd 100644
--- a/appview/state/signer.go
+++ b/appview/state/signer.go
@@ -174,3 +174,22 @@ func (s *SignedClient) Merge(patch []byte, ownerDid, targetRepo, branch string)
return s.client.Do(req)
}
+
+func (s *SignedClient) MergeCheck(patch []byte, ownerDid, targetRepo, branch string) (*http.Response, error) {
+ const (
+ Method = "POST"
+ )
+ endpoint := fmt.Sprintf("/%s/%s/merge/check", ownerDid, targetRepo)
+
+ body, _ := json.Marshal(map[string]interface{}{
+ "patch": string(patch),
+ "branch": branch,
+ })
+
+ req, err := s.newRequest(Method, endpoint, body)
+ if err != nil {
+ return nil, err
+ }
+
+ return s.client.Do(req)
+}
diff --git a/appview/state/state.go b/appview/state/state.go
index 7f142c26..7c0b4fb5 100644
--- a/appview/state/state.go
+++ b/appview/state/state.go
@@ -881,6 +881,17 @@ func (s *State) UserRouter() http.Handler {
r.Route("/pulls", func(r chi.Router) {
r.Get("/", s.RepoPulls)
+ r.Get("/{pull}", s.RepoSinglePull)
+
+ r.Group(func(r chi.Router) {
+ r.Use(AuthMiddleware(s))
+ r.Get("/new", s.NewPull)
+ r.Post("/new", s.NewPull)
+ // r.Post("/{pull}/comment", s.PullComment)
+ // r.Post("/{pull}/close", s.ClosePull)
+ // r.Post("/{pull}/reopen", s.ReopenPull)
+ // r.Post("/{pull}/merge", s.MergePull)
+ })
})
// These routes get proxied to the knot
diff --git a/cmd/gen.go b/cmd/gen.go
index 9e730119..2824054b 100644
--- a/cmd/gen.go
+++ b/cmd/gen.go
@@ -22,8 +22,8 @@ func main() {
shtangled.RepoIssueState{},
shtangled.RepoIssue{},
shtangled.Repo{},
- shtangled.RepoPullPatch{},
- shtangled.RepoPullState{},
+ shtangled.RepoPull{},
+ shtangled.RepoPullStatus{},
shtangled.RepoPullComment{},
); err != nil {
panic(err)