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

add `source` field to pull lexicon

Changed files
+666 -132
api
appview
db
pages
templates
state
cmd
knotserver
lexicons
pulls
types
+188 -44
api/tangled/cbor_gen.go
···
fieldCount--
-
if t.SourceRepo == nil {
+
if t.Source == nil {
fieldCount--
···
-
// t.CreatedAt (string) (string)
-
if t.CreatedAt != nil {
+
// t.Source (tangled.RepoPull_Source) (struct)
+
if t.Source != nil {
-
if len("createdAt") > 1000000 {
-
return xerrors.Errorf("Value in field \"createdAt\" was too long")
+
if len("source") > 1000000 {
+
return xerrors.Errorf("Value in field \"source\" was too long")
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil {
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil {
return err
-
if _, err := cw.WriteString(string("createdAt")); err != nil {
+
if _, err := cw.WriteString(string("source")); err != nil {
return err
-
if t.CreatedAt == nil {
-
if _, err := cw.Write(cbg.CborNull); err != nil {
-
return err
-
}
-
} else {
-
if len(*t.CreatedAt) > 1000000 {
-
return xerrors.Errorf("Value in field t.CreatedAt was too long")
-
}
-
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.CreatedAt))); err != nil {
-
return err
-
}
-
if _, err := cw.WriteString(string(*t.CreatedAt)); err != nil {
-
return err
-
}
+
if err := t.Source.MarshalCBOR(cw); err != nil {
+
return err
-
// t.SourceRepo (string) (string)
-
if t.SourceRepo != nil {
+
// t.CreatedAt (string) (string)
+
if t.CreatedAt != nil {
-
if len("sourceRepo") > 1000000 {
-
return xerrors.Errorf("Value in field \"sourceRepo\" was too long")
+
if len("createdAt") > 1000000 {
+
return xerrors.Errorf("Value in field \"createdAt\" was too long")
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sourceRepo"))); err != nil {
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil {
return err
-
if _, err := cw.WriteString(string("sourceRepo")); err != nil {
+
if _, err := cw.WriteString(string("createdAt")); err != nil {
return err
-
if t.SourceRepo == nil {
+
if t.CreatedAt == nil {
if _, err := cw.Write(cbg.CborNull); err != nil {
return err
} else {
-
if len(*t.SourceRepo) > 1000000 {
-
return xerrors.Errorf("Value in field t.SourceRepo was too long")
+
if len(*t.CreatedAt) > 1000000 {
+
return xerrors.Errorf("Value in field t.CreatedAt was too long")
-
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SourceRepo))); err != nil {
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.CreatedAt))); err != nil {
return err
-
if _, err := cw.WriteString(string(*t.SourceRepo)); err != nil {
+
if _, err := cw.WriteString(string(*t.CreatedAt)); err != nil {
return err
···
t.PullId = int64(extraI)
-
// t.CreatedAt (string) (string)
-
case "createdAt":
+
// t.Source (tangled.RepoPull_Source) (struct)
+
case "source":
+
b, err := cr.ReadByte()
if err != nil {
return err
···
if err := cr.UnreadByte(); err != nil {
return err
-
-
sval, err := cbg.ReadStringWithMax(cr, 1000000)
-
if err != nil {
-
return err
+
t.Source = new(RepoPull_Source)
+
if err := t.Source.UnmarshalCBOR(cr); err != nil {
+
return xerrors.Errorf("unmarshaling t.Source pointer: %w", err)
-
-
t.CreatedAt = (*string)(&sval)
+
-
// t.SourceRepo (string) (string)
-
case "sourceRepo":
+
// t.CreatedAt (string) (string)
+
case "createdAt":
b, err := cr.ReadByte()
···
return err
-
t.SourceRepo = (*string)(&sval)
+
t.CreatedAt = (*string)(&sval)
// t.TargetRepo (string) (string)
···
t.TargetBranch = string(sval)
+
}
+
+
default:
+
// Field doesn't exist on this type, so ignore it
+
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
+
return err
+
}
+
}
+
}
+
+
return nil
+
}
+
func (t *RepoPull_Source) MarshalCBOR(w io.Writer) error {
+
if t == nil {
+
_, err := w.Write(cbg.CborNull)
+
return err
+
}
+
+
cw := cbg.NewCborWriter(w)
+
fieldCount := 2
+
+
if t.Repo == nil {
+
fieldCount--
+
}
+
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
+
return err
+
}
+
+
// t.Repo (string) (string)
+
if t.Repo != nil {
+
+
if len("repo") > 1000000 {
+
return xerrors.Errorf("Value in field \"repo\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("repo")); err != nil {
+
return err
+
}
+
+
if t.Repo == nil {
+
if _, err := cw.Write(cbg.CborNull); err != nil {
+
return err
+
}
+
} else {
+
if len(*t.Repo) > 1000000 {
+
return xerrors.Errorf("Value in field t.Repo was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(*t.Repo)); err != nil {
+
return err
+
}
+
}
+
}
+
+
// t.Branch (string) (string)
+
if len("branch") > 1000000 {
+
return xerrors.Errorf("Value in field \"branch\" was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("branch"))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string("branch")); err != nil {
+
return err
+
}
+
+
if len(t.Branch) > 1000000 {
+
return xerrors.Errorf("Value in field t.Branch was too long")
+
}
+
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Branch))); err != nil {
+
return err
+
}
+
if _, err := cw.WriteString(string(t.Branch)); err != nil {
+
return err
+
}
+
return nil
+
}
+
+
func (t *RepoPull_Source) UnmarshalCBOR(r io.Reader) (err error) {
+
*t = RepoPull_Source{}
+
+
cr := cbg.NewCborReader(r)
+
+
maj, extra, err := cr.ReadHeader()
+
if err != nil {
+
return err
+
}
+
defer func() {
+
if err == io.EOF {
+
err = io.ErrUnexpectedEOF
+
}
+
}()
+
+
if maj != cbg.MajMap {
+
return fmt.Errorf("cbor input should be of type map")
+
}
+
+
if extra > cbg.MaxLength {
+
return fmt.Errorf("RepoPull_Source: map struct too large (%d)", extra)
+
}
+
+
n := extra
+
+
nameBuf := make([]byte, 6)
+
for i := uint64(0); i < n; i++ {
+
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
+
if err != nil {
+
return err
+
}
+
+
if !ok {
+
// Field doesn't exist on this type, so ignore it
+
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
+
return err
+
}
+
continue
+
}
+
+
switch string(nameBuf[:nameLen]) {
+
// t.Repo (string) (string)
+
case "repo":
+
+
{
+
b, err := cr.ReadByte()
+
if err != nil {
+
return err
+
}
+
if b != cbg.CborNull[0] {
+
if err := cr.UnreadByte(); err != nil {
+
return err
+
}
+
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Repo = (*string)(&sval)
+
}
+
}
+
// t.Branch (string) (string)
+
case "branch":
+
+
{
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
+
if err != nil {
+
return err
+
}
+
+
t.Branch = string(sval)
default:
+15 -9
api/tangled/repopull.go
···
} //
// RECORDTYPE: RepoPull
type RepoPull struct {
-
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"`
-
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
-
CreatedAt *string `json:"createdAt,omitempty" cborgen:"createdAt,omitempty"`
-
Patch string `json:"patch" cborgen:"patch"`
-
PullId int64 `json:"pullId" cborgen:"pullId"`
-
SourceRepo *string `json:"sourceRepo,omitempty" cborgen:"sourceRepo,omitempty"`
-
TargetBranch string `json:"targetBranch" cborgen:"targetBranch"`
-
TargetRepo string `json:"targetRepo" cborgen:"targetRepo"`
-
Title string `json:"title" cborgen:"title"`
+
LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"`
+
Body *string `json:"body,omitempty" cborgen:"body,omitempty"`
+
CreatedAt *string `json:"createdAt,omitempty" cborgen:"createdAt,omitempty"`
+
Patch string `json:"patch" cborgen:"patch"`
+
PullId int64 `json:"pullId" cborgen:"pullId"`
+
Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"`
+
TargetBranch string `json:"targetBranch" cborgen:"targetBranch"`
+
TargetRepo string `json:"targetRepo" cborgen:"targetRepo"`
+
Title string `json:"title" cborgen:"title"`
+
}
+
+
// RepoPull_Source is a "source" in the sh.tangled.repo.pull schema.
+
type RepoPull_Source struct {
+
Branch string `json:"branch" cborgen:"branch"`
+
Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"`
}
+8 -1
appview/db/db.go
···
})
runMigration(db, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
-
// add unconstrained column
_, err := tx.Exec(`
alter table comments add column deleted text; -- timestamp
alter table comments add column edited text; -- timestamp
+
`)
+
return err
+
})
+
+
runMigration(db, "add-source-info-to-pulls", 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;
`)
return err
})
+82 -7
appview/db/pulls.go
···
Submissions []*PullSubmission
// meta
-
Created time.Time
+
Created time.Time
+
PullSource *PullSource
+
}
+
+
type PullSource struct {
+
Branch string
+
Repo *syntax.ATURI
}
type PullSubmission struct {
···
func (p *Pull) LastRoundNumber() int {
return len(p.Submissions) - 1
+
}
+
+
func (p *Pull) IsSameRepoBranch() bool {
+
if p.PullSource != nil {
+
if p.PullSource.Repo != nil {
+
return p.PullSource.Repo == &p.RepoAt
+
} else {
+
// no repo specified
+
return true
+
}
+
}
+
return false
}
func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff {
···
pull.PullId = nextId
pull.State = PullOpen
-
_, err = tx.Exec(`
-
insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state)
-
values (?, ?, ?, ?, ?, ?, ?, ?)
-
`, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.TargetBranch, pull.Body, pull.Rkey, pull.State)
+
var sourceBranch, sourceRepoAt *string
+
if pull.PullSource != nil {
+
sourceBranch = &pull.PullSource.Branch
+
if pull.PullSource.Repo != nil {
+
x := pull.PullSource.Repo.String()
+
sourceRepoAt = &x
+
}
+
}
+
+
_, err = tx.Exec(
+
`
+
insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state, source_branch, source_repo_at)
+
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+
pull.RepoAt,
+
pull.OwnerDid,
+
pull.PullId,
+
pull.Title,
+
pull.TargetBranch,
+
pull.Body,
+
pull.Rkey,
+
pull.State,
+
sourceBranch,
+
sourceRepoAt,
+
)
if err != nil {
return err
}
···
target_branch,
pull_at,
body,
-
rkey
+
rkey,
+
source_branch,
+
source_repo_at
from
pulls
where
···
for rows.Next() {
var pull Pull
var createdAt string
+
var sourceBranch, sourceRepoAt sql.NullString
err := rows.Scan(
&pull.OwnerDid,
&pull.PullId,
···
&pull.PullAt,
&pull.Body,
&pull.Rkey,
+
&sourceBranch,
+
&sourceRepoAt,
)
if err != nil {
return nil, err
···
}
pull.Created = createdTime
+
if sourceBranch.Valid {
+
pull.PullSource = &PullSource{
+
Branch: sourceBranch.String,
+
}
+
if sourceRepoAt.Valid {
+
sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String)
+
if err != nil {
+
return nil, err
+
}
+
pull.PullSource.Repo = &sourceRepoAtParsed
+
}
+
}
+
pulls = append(pulls, pull)
}
···
pull_at,
repo_at,
body,
-
rkey
+
rkey,
+
source_branch,
+
source_repo_at
from
pulls
where
···
var pull Pull
var createdAt string
+
var sourceBranch, sourceRepoAt sql.NullString
err := row.Scan(
&pull.OwnerDid,
&pull.PullId,
···
&pull.RepoAt,
&pull.Body,
&pull.Rkey,
+
&sourceBranch,
+
&sourceRepoAt,
)
if err != nil {
return nil, err
···
return nil, err
}
pull.Created = createdTime
+
+
// populate source
+
if sourceBranch.Valid {
+
pull.PullSource = &PullSource{
+
Branch: sourceBranch.String,
+
}
+
if sourceRepoAt.Valid {
+
sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String)
+
if err != nil {
+
return nil, err
+
}
+
pull.PullSource.Repo = &sourceRepoAtParsed
+
}
+
}
submissionsQuery := `
select
+2 -3
appview/pages/pages.go
···
RepoInfo RepoInfo
Active string
DidHandleMap map[string]string
-
-
Pull db.Pull
-
MergeCheck types.MergeCheckResponse
+
Pull *db.Pull
+
MergeCheck types.MergeCheckResponse
}
func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
+15 -8
appview/pages/templates/fragments/pullActions.html
···
{{ $isConflicted := and .MergeCheck (or .MergeCheck.Error .MergeCheck.IsConflicted) }}
{{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }}
{{ $isLastRound := eq $roundNumber $lastIdx }}
+
{{ $isSameRepoBranch := .Pull.IsSameRepoBranch }}
<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 }}
-
<button
-
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
-
hx-target="#actions-{{$roundNumber}}"
-
hx-swap="outerHtml"
-
class="btn p-2 flex items-center gap-2">
-
{{ i "rotate-ccw" "w-4 h-4" }}
-
<span>resubmit</span>
-
</button>
+
<button id="resubmitBtn"
+
{{ if $isSameRepoBranch }}
+
hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
+
{{ else }}
+
hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit"
+
hx-target="#actions-{{$roundNumber}}"
+
hx-swap="outerHtml"
+
{{ end }}
+
+
hx-disabled-elt="#resubmitBtn"
+
class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
+
{{ i "rotate-ccw" "w-4 h-4" }}
+
<span>resubmit</span>
+
</button>
{{ end }}
{{ if and (or $isPullAuthor $isPushAllowed) $isOpen $isLastRound }}
+73 -39
appview/pages/templates/repo/pulls/new.html
···
<ul class="list-decimal pl-10 space-y-2 text-gray-700 dark:text-gray-300">
<li class="leading-relaxed">Clone this repository.</li>
<li class="leading-relaxed">Make your changes in your local repository.</li>
-
<li class="leading-relaxed">Grab the diff using <code class="bg-gray-100 dark:bg-gray-700 px-1 py-0.5 rounded text-gray-800 dark:text-gray-200 font-mono text-sm">git diff</code>.</li>
+
<li class="leading-relaxed">Grab the diff using <code>git diff</code>.</li>
<li class="leading-relaxed">Paste the diff output in the form below.</li>
</ul>
</p>
···
hx-swap="none"
>
<div class="flex flex-col gap-4">
-
<div>
-
<label for="title" class="dark:text-white">write a title</label>
-
<input type="text" name="title" id="title" class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600" />
+
<div>
+
<label for="title" class="dark:text-white">write a title</label>
+
<input type="text" name="title" id="title" class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600" />
+
</div>
-
<label for="targetBranch" class="dark:text-white">select a target branch</label>
-
<p class="text-gray-500 dark:text-gray-400">
-
The branch you want to make your change against.
-
</p>
-
<select
-
name="targetBranch"
-
class="p-1 mb-2 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
-
>
-
<option disabled selected>select a branch</option>
-
{{ range .Branches }}
-
<option value="{{ .Reference.Name }}" class="py-1">
-
{{ .Reference.Name }}
-
</option>
-
{{ end }}
-
</select>
-
<label for="body" class="dark:text-white">add a description</label>
-
<textarea
-
name="body"
-
id="body"
-
rows="6"
-
class="w-full resize-y dark:bg-gray-700 dark:text-white dark:border-gray-600"
-
placeholder="Describe your change. Markdown is supported."
+
<div>
+
<label for="body" class="dark:text-white">add a description</label>
+
<textarea
+
name="body"
+
id="body"
+
rows="6"
+
class="w-full resize-y dark:bg-gray-700 dark:text-white dark:border-gray-600"
+
placeholder="Describe your change. Markdown is supported."
></textarea>
+
</div>
-
<div class="mt-4">
-
<label for="patch" class="dark:text-white">paste your patch here</label>
-
<textarea
-
name="patch"
-
id="patch"
-
rows="10"
-
class="w-full resize-y font-mono dark:bg-gray-700 dark:text-white dark:border-gray-600"
-
placeholder="Paste your git diff output here."
-
></textarea>
-
</div>
-
</div>
-
<div>
-
<button type="submit" class="btn dark:bg-gray-600 dark:hover:bg-gray-500 dark:text-white">create</button>
+
<div>
+
<label for="targetBranch" class="dark:text-white">configure branches</label>
+
<div class="flex flex-wrap gap-2 items-center">
+
<select
+
required
+
name="targetBranch"
+
class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
+
>
+
<option disabled selected>target branch</option>
+
{{ range .Branches }}
+
<option value="{{ .Reference.Name }}" class="py-1">
+
{{ .Reference.Name }}
+
</option>
+
{{ end }}
+
</select>
+
+
{{ if .RepoInfo.Roles.IsPushAllowed }}
+
{{ i "move-left" "w-5 h-5" }}
+
<select
+
name="sourceBranch"
+
class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600"
+
>
+
<option disabled selected>source branch</option>
+
{{ range .Branches }}
+
<option value="{{ .Reference.Name }}" class="py-1">
+
{{ .Reference.Name }}
+
</option>
+
{{ end }}
+
</select>
+
{{ end }}
+
</div>
+
</div>
+
+
<div class="mt-4">
+
{{ $label := "paste your patch here" }}
+
{{ $rows := 10 }}
+
{{ if .RepoInfo.Roles.IsPushAllowed }}
+
{{ $label = "or paste your patch here" }}
+
{{ $rows = 4 }}
+
{{ end }}
+
+
<label for="patch" class="dark:text-white">{{ $label }}</label>
+
<textarea
+
name="patch"
+
id="patch"
+
rows="{{$rows}}"
+
class="w-full resize-y font-mono dark:bg-gray-700 dark:text-white dark:border-gray-600"
+
placeholder="Paste your git diff output here."
+
></textarea>
+
</div>
+
+
<div class="flex justify-end items-center gap-2">
+
<button type="submit" class="btn">create</button>
+
</div>
+
</div>
<div id="pull" class="error dark:text-red-300"></div>
</form>
{{ end }}
+
+
{{ define "repoAfter" }}
+
<div id="patch-preview" class="error dark:text-red-300"></div>
+
{{ end }}
+9 -1
appview/pages/templates/repo/pulls/pull.html
···
<span class="select-none before:content-['\00B7']"></span>
<time>{{ .Pull.Created | timeFmt }}</time>
<span class="select-none before:content-['\00B7']"></span>
-
<span>targeting branch
+
<span>
+
targeting
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
{{ .Pull.TargetBranch }}
</span>
</span>
+
{{ if .Pull.IsSameRepoBranch }}
+
<span>from
+
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
+
{{ .Pull.PullSource.Branch }}
+
</span>
+
</span>
+
{{ end }}
</span>
</div>
+8 -1
appview/pages/templates/repo/pulls/pulls.html
···
</span>
<span class="before:content-['·']">
-
targeting branch
+
targeting
<span class="text-xs rounded bg-gray-100 dark:bg-gray-600 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
{{ .TargetBranch }}
</span>
</span>
+
{{ if .IsSameRepoBranch }}
+
<span>from
+
<span class="text-xs rounded bg-gray-100 dark:bg-gray-700 text-black dark:text-white font-mono px-2 mx-1/2 inline-flex items-center">
+
{{ .PullSource.Branch }}
+
</span>
+
</span>
+
{{ end }}
</p>
</div>
{{ end }}
+103 -3
appview/state/pull.go
···
LoggedInUser: user,
RepoInfo: f.RepoInfo(s, user),
DidHandleMap: didHandleMap,
-
Pull: *pull,
+
Pull: pull,
MergeCheck: mergeCheckResponse,
})
}
···
Branches: result.Branches,
})
case http.MethodPost:
+
isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed()
title := r.FormValue("title")
body := r.FormValue("body")
targetBranch := r.FormValue("targetBranch")
+
sourceBranch := r.FormValue("sourceBranch")
patch := r.FormValue("patch")
-
if title == "" || body == "" || patch == "" || targetBranch == "" {
-
s.pages.Notice(w, "pull", "Title, body and patch diff are required.")
+
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.")
return
}
+
if patch != "" && sourceBranch != "" {
+
s.pages.Notice(w, "pull", "Cannot select both patch and source branch.")
+
return
+
}
+
+
if title == "" || body == "" || targetBranch == "" {
+
s.pages.Notice(w, "pull", "Title, body and target branch are required.")
+
return
+
}
+
+
// TODO: check if knot has this capability
+
var pullSource *db.PullSource
+
if sourceBranch != "" && isPushAllowed {
+
pullSource = &db.PullSource{
+
Branch: sourceBranch,
+
}
+
// generate a patch using /compare
+
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
+
if err != nil {
+
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
+
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 {
+
case 404:
+
case 400:
+
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
+
}
+
+
respBody, err := io.ReadAll(resp.Body)
+
if err != nil {
+
log.Println("failed to compare across branches")
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
+
}
+
defer resp.Body.Close()
+
+
var diffTreeResponse types.RepoDiffTreeResponse
+
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.")
+
}
+
+
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.")
···
Submissions: []*db.PullSubmission{
&initialSubmission,
},
+
PullSource: pullSource,
})
if err != nil {
log.Println("failed to create pull request", err)
···
return
case http.MethodPost:
patch := r.FormValue("patch")
+
+
// 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
+
// extract patch by performing compare
+
ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev)
+
if err != nil {
+
log.Printf("failed to create signed client for %s: %s", f.Knot, err)
+
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 {
+
case 404:
+
case 400:
+
s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.")
+
}
+
+
respBody, err := io.ReadAll(resp.Body)
+
if err != nil {
+
log.Println("failed to compare across branches")
+
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
+
}
+
defer resp.Body.Close()
+
+
var diffTreeResponse types.RepoDiffTreeResponse
+
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.")
+
}
+
+
patch = diffTreeResponse.DiffTree.Patch
+
}
if patch == "" {
s.pages.Notice(w, "resubmit-error", "Patch is empty.")
+15
appview/state/signer.go
···
return us.client.Do(req)
}
+
+
func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*http.Response, error) {
+
const (
+
Method = "GET"
+
)
+
+
endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, rev1, rev2)
+
+
req, err := us.newRequest(Method, endpoint, nil)
+
if err != nil {
+
return nil, err
+
}
+
+
return us.client.Do(req)
+
}
+1
cmd/gen.go
···
shtangled.RepoIssue{},
shtangled.Repo{},
shtangled.RepoPull{},
+
shtangled.RepoPull_Source{},
shtangled.RepoPullStatus{},
shtangled.RepoPullComment{},
); err != nil {
+71 -10
knotserver/git/diff.go
···
"strings"
"github.com/bluekeyes/go-gitdiff/gitdiff"
+
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"tangled.sh/tangled.sh/core/types"
)
···
}
nd := types.NiceDiff{}
-
nd.Commit.This = c.Hash.String()
-
-
if parent.Hash.IsZero() {
-
nd.Commit.Parent = ""
-
} else {
-
nd.Commit.Parent = parent.Hash.String()
-
}
-
nd.Commit.Author = c.Author
-
nd.Commit.Message = c.Message
-
for _, d := range diffs {
ndiff := types.Diff{}
ndiff.Name.New = d.NewName
···
}
nd.Stat.FilesChanged = len(diffs)
+
nd.Commit.This = c.Hash.String()
+
+
if parent.Hash.IsZero() {
+
nd.Commit.Parent = ""
+
} else {
+
nd.Commit.Parent = parent.Hash.String()
+
}
+
nd.Commit.Author = c.Author
+
nd.Commit.Message = c.Message
return &nd, nil
}
+
+
func (g *GitRepo) DiffTree(rev1, rev2 string) (*types.DiffTree, error) {
+
commit1, err := g.resolveRevision(rev1)
+
if err != nil {
+
return nil, fmt.Errorf("Invalid revision: %s", rev1)
+
}
+
+
commit2, err := g.resolveRevision(rev2)
+
if err != nil {
+
return nil, fmt.Errorf("Invalid revision: %s", rev2)
+
}
+
+
log.Println(commit1, commit2)
+
+
tree1, err := commit1.Tree()
+
if err != nil {
+
return nil, err
+
}
+
+
tree2, err := commit2.Tree()
+
if err != nil {
+
return nil, err
+
}
+
+
diff, err := object.DiffTree(tree1, tree2)
+
if err != nil {
+
return nil, err
+
}
+
+
patch, err := diff.Patch()
+
if err != nil {
+
return nil, err
+
}
+
+
diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String()))
+
if err != nil {
+
return nil, err
+
}
+
+
return &types.DiffTree{
+
Rev1: commit1.Hash.String(),
+
Rev2: commit2.Hash.String(),
+
Patch: patch.String(),
+
Diff: diffs,
+
}, nil
+
}
+
+
func (g *GitRepo) resolveRevision(revStr string) (*object.Commit, error) {
+
rev, err := g.r.ResolveRevision(plumbing.Revision(revStr))
+
if err != nil {
+
return nil, fmt.Errorf("resolving revision %s: %w", revStr, err)
+
}
+
+
commit, err := g.r.CommitObject(*rev)
+
if err != nil {
+
+
return nil, fmt.Errorf("getting commit for %s: %w", revStr, err)
+
}
+
+
return commit, nil
+
}
+10
knotserver/git/git.go
···
return &g, nil
}
+
func PlainOpen(path string) (*GitRepo, error) {
+
var err error
+
g := GitRepo{path: path}
+
g.r, err = git.PlainOpen(path)
+
if err != nil {
+
return nil, fmt.Errorf("opening %s: %w", path, err)
+
}
+
return &g, nil
+
}
+
func (g *GitRepo) Commits() ([]*object.Commit, error) {
ci, err := g.r.Log(&git.LogOptions{From: g.h})
if err != nil {
+1
knotserver/handler.go
···
r.Get("/", h.RepoIndex)
r.Get("/info/refs", h.InfoRefs)
r.Post("/git-upload-pack", h.UploadPack)
+
r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects
r.Route("/merge", func(r chi.Router) {
r.With(h.VerifySignature)
+30 -1
knotserver/routes.go
···
capabilities := map[string]any{
"pull_requests": map[string]any{
-
"patch_submissions": true,
+
"patch_submissions": true,
+
"branch_submissions": true,
+
"fork_submissions": false,
},
}
···
}
writeError(w, err.Error(), http.StatusInternalServerError)
h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error())
+
}
+
+
func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) {
+
rev1 := chi.URLParam(r, "rev1")
+
rev1, _ = url.PathUnescape(rev1)
+
+
rev2 := chi.URLParam(r, "rev2")
+
rev2, _ = url.PathUnescape(rev2)
+
+
l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2)
+
+
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
+
gr, err := git.PlainOpen(path)
+
if err != nil {
+
notFound(w)
+
return
+
}
+
+
difftree, err := gr.DiffTree(rev1, rev2)
+
if err != nil {
+
l.Error("error comparing revisions", "msg", err.Error())
+
writeError(w, "error comparing revisions", http.StatusBadRequest)
+
return
+
}
+
+
writeJSON(w, types.RepoDiffTreeResponse{difftree})
+
return
}
func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
+24 -5
lexicons/pulls/pull.json
···
"key": "tid",
"record": {
"type": "object",
-
"required": ["targetRepo", "targetBranch", "pullId", "title", "patch"],
+
"required": [
+
"targetRepo",
+
"targetBranch",
+
"pullId",
+
"title",
+
"patch"
+
],
"properties": {
"targetRepo": {
"type": "string",
···
"targetBranch": {
"type": "string"
},
-
"sourceRepo": {
-
"type": "string",
-
"format": "at-uri"
-
},
"pullId": {
"type": "integer"
},
···
},
"patch": {
"type": "string"
+
},
+
"source": {
+
"type": "ref",
+
"ref": "#source"
}
+
}
+
}
+
},
+
"source": {
+
"type": "object",
+
"required": ["branch"],
+
"properties": {
+
"branch": {
+
"type": "string"
+
},
+
"repo": {
+
"type": "string",
+
"format": "at-uri"
}
}
}
+7
types/diff.go
···
} `json:"stat"`
Diff []Diff `json:"diff"`
}
+
+
type DiffTree struct {
+
Rev1 string `json:"rev1"`
+
Rev2 string `json:"rev2"`
+
Patch string `json:"patch"`
+
Diff []*gitdiff.File `json:"diff"`
+
}
+4
types/repo.go
···
Diff *NiceDiff `json:"diff,omitempty"`
}
+
type RepoDiffTreeResponse struct {
+
DiffTree *DiffTree `json:"difftree,omitempty"`
+
}
+
type RepoTreeResponse struct {
Ref string `json:"ref,omitempty"`
Parent string `json:"parent,omitempty"`