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

appview: implement some basic pull request handlers

Changed files
+501 -52
appview
db
pages
templates
repo
state
cmd
+4 -1
appview/db/db.go
···
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),
+46 -43
appview/db/pulls.go
···
"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 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 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"`
+
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 *Pulls) error {
+
func NewPull(tx *sql.Tx, pull *Pull) error {
defer tx.Rollback()
_, err := tx.Exec(`
···
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
}
···
}
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
}
···
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
}
···
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
}
···
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
}
···
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,
···
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
···
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 {
+27
appview/pages/pages.go
···
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 {
params.Active = "pulls"
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 {
+38
appview/pages/templates/repo/pulls/new.html
···
+
{{ define "title" }}new pull | {{ .RepoInfo.FullName }}{{ end }}
+
+
{{ define "repoContent" }}
+
<form
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/new"
+
class="mt-6 space-y-6"
+
hx-swap="none"
+
>
+
<div class="flex flex-col gap-4">
+
<div>
+
<label for="title">title</label>
+
<input type="text" name="title" id="title" class="w-full" />
+
<input type="text" name="targetBranch" id="targetBranch" />
+
</div>
+
<div>
+
<label for="body">body</label>
+
<textarea
+
name="body"
+
id="body"
+
rows="6"
+
class="w-full resize-y"
+
placeholder="Describe your change. Markdown is supported."
+
></textarea>
+
<textarea
+
name="patch"
+
id="patch"
+
rows="10"
+
class="w-full resize-y font-mono"
+
placeholder="Paste your git-format-patch output here."
+
></textarea>
+
</div>
+
<div>
+
<button type="submit" class="btn">create</button>
+
</div>
+
</div>
+
<div id="pull" class="error"></div>
+
</form>
+
{{ end }}
+133
appview/pages/templates/repo/pulls/pull.html
···
+
{{ define "title" }}
+
{{ .Pull.Title }} &middot;
+
{{ .RepoInfo.FullName }}
+
{{ end }}
+
+
{{ define "repoContent" }}
+
<h1>
+
{{ .Pull.Title }}
+
<span class="text-gray-400">#{{ .Pull.PullId }}</span>
+
</h1>
+
+
{{ $bgColor := "bg-gray-800" }}
+
{{ $icon := "ban" }}
+
{{ if eq .State "open" }}
+
{{ $bgColor = "bg-green-600" }}
+
{{ $icon = "circle-dot" }}
+
{{ end }}
+
+
+
<section>
+
<div class="flex items-center gap-2">
+
<div
+
id="state"
+
class="inline-flex items-center rounded px-3 py-1 {{ $bgColor }}"
+
>
+
<i
+
data-lucide="{{ $icon }}"
+
class="w-4 h-4 mr-1.5 text-white"
+
></i>
+
<span class="text-white">{{ .State }}</span>
+
</div>
+
<span class="text-gray-400 text-sm">
+
opened by
+
{{ $owner := didOrHandle .Pull.OwnerDid .PullOwnerHandle }}
+
<a href="/{{ $owner }}" class="no-underline hover:underline"
+
>{{ $owner }}</a
+
>
+
<span class="px-1 select-none before:content-['\00B7']"></span>
+
<time>{{ .Pull.Created | timeFmt }}</time>
+
</span>
+
</div>
+
+
{{ if .Pull.Body }}
+
<article id="body" class="mt-8 prose">
+
{{ .Pull.Body | markdown }}
+
</article>
+
{{ end }}
+
</section>
+
{{ end }}
+
+
{{ define "repoAfter" }}
+
<section id="comments" class="mt-8 space-y-4 relative">
+
{{ range $index, $comment := .Comments }}
+
<div
+
id="comment-{{ .CommentId }}"
+
class="rounded bg-white p-4 relative"
+
>
+
{{ if eq $index 0 }}
+
<div
+
class="absolute left-8 -top-8 w-px h-8 bg-gray-300"
+
></div>
+
{{ else }}
+
<div
+
class="absolute left-8 -top-4 w-px h-4 bg-gray-300"
+
></div>
+
{{ end }}
+
<div class="flex items-center gap-2 mb-2 text-gray-400">
+
{{ $owner := index $.DidHandleMap .OwnerDid }}
+
<span class="text-sm">
+
<a
+
href="/{{ $owner }}"
+
class="no-underline hover:underline"
+
>{{ $owner }}</a
+
>
+
</span>
+
<span
+
class="px-1 select-none before:content-['\00B7']"
+
></span>
+
<a
+
href="#{{ .CommentId }}"
+
class="text-gray-500 text-sm hover:text-gray-500 hover:underline no-underline"
+
id="{{ .CommentId }}"
+
>
+
{{ .Created | timeFmt }}
+
</a>
+
</div>
+
<div class="prose">
+
{{ .Body | markdown }}
+
</div>
+
</div>
+
{{ end }}
+
</section>
+
+
{{ if .LoggedInUser }}
+
<form
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/comment"
+
class="mt-8"
+
>
+
<textarea
+
name="body"
+
class="w-full p-2 rounded border border-gray-200"
+
placeholder="Add to the discussion..."
+
></textarea>
+
<button type="submit" class="btn mt-2">comment</button>
+
<div id="pull-comment"></div>
+
</form>
+
{{ 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 }}
+
<form
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/{{ $action }}"
+
hx-swap="none"
+
class="mt-8"
+
>
+
<button type="submit" class="btn hover:bg-{{ $hoverColor }}-300">
+
<i
+
data-lucide="{{ $icon }}"
+
class="w-4 h-4 mr-2 text-{{ $hoverColor }}-400"
+
></i>
+
<span class="text-black">{{ $action }}</span>
+
</button>
+
<div id="pull-action" class="error"></div>
+
</form>
+
{{ end }}
+
{{ end }}
+221 -6
appview/state/repo.go
···
}
}
+
// 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 {
···
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) {
+19
appview/state/signer.go
···
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)
+
}
+11
appview/state/state.go
···
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
+2 -2
cmd/gen.go
···
shtangled.RepoIssueState{},
shtangled.RepoIssue{},
shtangled.Repo{},
-
shtangled.RepoPullPatch{},
-
shtangled.RepoPullState{},
+
shtangled.RepoPull{},
+
shtangled.RepoPullStatus{},
shtangled.RepoPullComment{},
); err != nil {
panic(err)