Monorepo for Tangled tangled.org

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
··· 2076 2076 fieldCount-- 2077 2077 } 2078 2078 2079 - if t.SourceRepo == nil { 2079 + if t.Source == nil { 2080 2080 fieldCount-- 2081 2081 } 2082 2082 ··· 2203 2203 } 2204 2204 } 2205 2205 2206 - // t.CreatedAt (string) (string) 2207 - if t.CreatedAt != nil { 2206 + // t.Source (tangled.RepoPull_Source) (struct) 2207 + if t.Source != nil { 2208 2208 2209 - if len("createdAt") > 1000000 { 2210 - return xerrors.Errorf("Value in field \"createdAt\" was too long") 2209 + if len("source") > 1000000 { 2210 + return xerrors.Errorf("Value in field \"source\" was too long") 2211 2211 } 2212 2212 2213 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 2213 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("source"))); err != nil { 2214 2214 return err 2215 2215 } 2216 - if _, err := cw.WriteString(string("createdAt")); err != nil { 2216 + if _, err := cw.WriteString(string("source")); err != nil { 2217 2217 return err 2218 2218 } 2219 2219 2220 - if t.CreatedAt == nil { 2221 - if _, err := cw.Write(cbg.CborNull); err != nil { 2222 - return err 2223 - } 2224 - } else { 2225 - if len(*t.CreatedAt) > 1000000 { 2226 - return xerrors.Errorf("Value in field t.CreatedAt was too long") 2227 - } 2228 - 2229 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.CreatedAt))); err != nil { 2230 - return err 2231 - } 2232 - if _, err := cw.WriteString(string(*t.CreatedAt)); err != nil { 2233 - return err 2234 - } 2220 + if err := t.Source.MarshalCBOR(cw); err != nil { 2221 + return err 2235 2222 } 2236 2223 } 2237 2224 2238 - // t.SourceRepo (string) (string) 2239 - if t.SourceRepo != nil { 2225 + // t.CreatedAt (string) (string) 2226 + if t.CreatedAt != nil { 2240 2227 2241 - if len("sourceRepo") > 1000000 { 2242 - return xerrors.Errorf("Value in field \"sourceRepo\" was too long") 2228 + if len("createdAt") > 1000000 { 2229 + return xerrors.Errorf("Value in field \"createdAt\" was too long") 2243 2230 } 2244 2231 2245 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sourceRepo"))); err != nil { 2232 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { 2246 2233 return err 2247 2234 } 2248 - if _, err := cw.WriteString(string("sourceRepo")); err != nil { 2235 + if _, err := cw.WriteString(string("createdAt")); err != nil { 2249 2236 return err 2250 2237 } 2251 2238 2252 - if t.SourceRepo == nil { 2239 + if t.CreatedAt == nil { 2253 2240 if _, err := cw.Write(cbg.CborNull); err != nil { 2254 2241 return err 2255 2242 } 2256 2243 } else { 2257 - if len(*t.SourceRepo) > 1000000 { 2258 - return xerrors.Errorf("Value in field t.SourceRepo was too long") 2244 + if len(*t.CreatedAt) > 1000000 { 2245 + return xerrors.Errorf("Value in field t.CreatedAt was too long") 2259 2246 } 2260 2247 2261 - if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.SourceRepo))); err != nil { 2248 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.CreatedAt))); err != nil { 2262 2249 return err 2263 2250 } 2264 - if _, err := cw.WriteString(string(*t.SourceRepo)); err != nil { 2251 + if _, err := cw.WriteString(string(*t.CreatedAt)); err != nil { 2265 2252 return err 2266 2253 } 2267 2254 } ··· 2436 2423 2437 2424 t.PullId = int64(extraI) 2438 2425 } 2439 - // t.CreatedAt (string) (string) 2440 - case "createdAt": 2426 + // t.Source (tangled.RepoPull_Source) (struct) 2427 + case "source": 2441 2428 2442 2429 { 2430 + 2443 2431 b, err := cr.ReadByte() 2444 2432 if err != nil { 2445 2433 return err ··· 2448 2436 if err := cr.UnreadByte(); err != nil { 2449 2437 return err 2450 2438 } 2451 - 2452 - sval, err := cbg.ReadStringWithMax(cr, 1000000) 2453 - if err != nil { 2454 - return err 2439 + t.Source = new(RepoPull_Source) 2440 + if err := t.Source.UnmarshalCBOR(cr); err != nil { 2441 + return xerrors.Errorf("unmarshaling t.Source pointer: %w", err) 2455 2442 } 2456 - 2457 - t.CreatedAt = (*string)(&sval) 2458 2443 } 2444 + 2459 2445 } 2460 - // t.SourceRepo (string) (string) 2461 - case "sourceRepo": 2446 + // t.CreatedAt (string) (string) 2447 + case "createdAt": 2462 2448 2463 2449 { 2464 2450 b, err := cr.ReadByte() ··· 2475 2461 return err 2476 2462 } 2477 2463 2478 - t.SourceRepo = (*string)(&sval) 2464 + t.CreatedAt = (*string)(&sval) 2479 2465 } 2480 2466 } 2481 2467 // t.TargetRepo (string) (string) ··· 2499 2485 } 2500 2486 2501 2487 t.TargetBranch = string(sval) 2488 + } 2489 + 2490 + default: 2491 + // Field doesn't exist on this type, so ignore it 2492 + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { 2493 + return err 2494 + } 2495 + } 2496 + } 2497 + 2498 + return nil 2499 + } 2500 + func (t *RepoPull_Source) MarshalCBOR(w io.Writer) error { 2501 + if t == nil { 2502 + _, err := w.Write(cbg.CborNull) 2503 + return err 2504 + } 2505 + 2506 + cw := cbg.NewCborWriter(w) 2507 + fieldCount := 2 2508 + 2509 + if t.Repo == nil { 2510 + fieldCount-- 2511 + } 2512 + 2513 + if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil { 2514 + return err 2515 + } 2516 + 2517 + // t.Repo (string) (string) 2518 + if t.Repo != nil { 2519 + 2520 + if len("repo") > 1000000 { 2521 + return xerrors.Errorf("Value in field \"repo\" was too long") 2522 + } 2523 + 2524 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("repo"))); err != nil { 2525 + return err 2526 + } 2527 + if _, err := cw.WriteString(string("repo")); err != nil { 2528 + return err 2529 + } 2530 + 2531 + if t.Repo == nil { 2532 + if _, err := cw.Write(cbg.CborNull); err != nil { 2533 + return err 2534 + } 2535 + } else { 2536 + if len(*t.Repo) > 1000000 { 2537 + return xerrors.Errorf("Value in field t.Repo was too long") 2538 + } 2539 + 2540 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.Repo))); err != nil { 2541 + return err 2542 + } 2543 + if _, err := cw.WriteString(string(*t.Repo)); err != nil { 2544 + return err 2545 + } 2546 + } 2547 + } 2548 + 2549 + // t.Branch (string) (string) 2550 + if len("branch") > 1000000 { 2551 + return xerrors.Errorf("Value in field \"branch\" was too long") 2552 + } 2553 + 2554 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("branch"))); err != nil { 2555 + return err 2556 + } 2557 + if _, err := cw.WriteString(string("branch")); err != nil { 2558 + return err 2559 + } 2560 + 2561 + if len(t.Branch) > 1000000 { 2562 + return xerrors.Errorf("Value in field t.Branch was too long") 2563 + } 2564 + 2565 + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Branch))); err != nil { 2566 + return err 2567 + } 2568 + if _, err := cw.WriteString(string(t.Branch)); err != nil { 2569 + return err 2570 + } 2571 + return nil 2572 + } 2573 + 2574 + func (t *RepoPull_Source) UnmarshalCBOR(r io.Reader) (err error) { 2575 + *t = RepoPull_Source{} 2576 + 2577 + cr := cbg.NewCborReader(r) 2578 + 2579 + maj, extra, err := cr.ReadHeader() 2580 + if err != nil { 2581 + return err 2582 + } 2583 + defer func() { 2584 + if err == io.EOF { 2585 + err = io.ErrUnexpectedEOF 2586 + } 2587 + }() 2588 + 2589 + if maj != cbg.MajMap { 2590 + return fmt.Errorf("cbor input should be of type map") 2591 + } 2592 + 2593 + if extra > cbg.MaxLength { 2594 + return fmt.Errorf("RepoPull_Source: map struct too large (%d)", extra) 2595 + } 2596 + 2597 + n := extra 2598 + 2599 + nameBuf := make([]byte, 6) 2600 + for i := uint64(0); i < n; i++ { 2601 + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) 2602 + if err != nil { 2603 + return err 2604 + } 2605 + 2606 + if !ok { 2607 + // Field doesn't exist on this type, so ignore it 2608 + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { 2609 + return err 2610 + } 2611 + continue 2612 + } 2613 + 2614 + switch string(nameBuf[:nameLen]) { 2615 + // t.Repo (string) (string) 2616 + case "repo": 2617 + 2618 + { 2619 + b, err := cr.ReadByte() 2620 + if err != nil { 2621 + return err 2622 + } 2623 + if b != cbg.CborNull[0] { 2624 + if err := cr.UnreadByte(); err != nil { 2625 + return err 2626 + } 2627 + 2628 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2629 + if err != nil { 2630 + return err 2631 + } 2632 + 2633 + t.Repo = (*string)(&sval) 2634 + } 2635 + } 2636 + // t.Branch (string) (string) 2637 + case "branch": 2638 + 2639 + { 2640 + sval, err := cbg.ReadStringWithMax(cr, 1000000) 2641 + if err != nil { 2642 + return err 2643 + } 2644 + 2645 + t.Branch = string(sval) 2502 2646 } 2503 2647 2504 2648 default:
+15 -9
api/tangled/repopull.go
··· 17 17 } // 18 18 // RECORDTYPE: RepoPull 19 19 type RepoPull struct { 20 - LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 - Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 - CreatedAt *string `json:"createdAt,omitempty" cborgen:"createdAt,omitempty"` 23 - Patch string `json:"patch" cborgen:"patch"` 24 - PullId int64 `json:"pullId" cborgen:"pullId"` 25 - SourceRepo *string `json:"sourceRepo,omitempty" cborgen:"sourceRepo,omitempty"` 26 - TargetBranch string `json:"targetBranch" cborgen:"targetBranch"` 27 - TargetRepo string `json:"targetRepo" cborgen:"targetRepo"` 28 - Title string `json:"title" cborgen:"title"` 20 + LexiconTypeID string `json:"$type,const=sh.tangled.repo.pull" cborgen:"$type,const=sh.tangled.repo.pull"` 21 + Body *string `json:"body,omitempty" cborgen:"body,omitempty"` 22 + CreatedAt *string `json:"createdAt,omitempty" cborgen:"createdAt,omitempty"` 23 + Patch string `json:"patch" cborgen:"patch"` 24 + PullId int64 `json:"pullId" cborgen:"pullId"` 25 + Source *RepoPull_Source `json:"source,omitempty" cborgen:"source,omitempty"` 26 + TargetBranch string `json:"targetBranch" cborgen:"targetBranch"` 27 + TargetRepo string `json:"targetRepo" cborgen:"targetRepo"` 28 + Title string `json:"title" cborgen:"title"` 29 + } 30 + 31 + // RepoPull_Source is a "source" in the sh.tangled.repo.pull schema. 32 + type RepoPull_Source struct { 33 + Branch string `json:"branch" cborgen:"branch"` 34 + Repo *string `json:"repo,omitempty" cborgen:"repo,omitempty"` 29 35 }
+8 -1
appview/db/db.go
··· 257 257 }) 258 258 259 259 runMigration(db, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error { 260 - // add unconstrained column 261 260 _, err := tx.Exec(` 262 261 alter table comments add column deleted text; -- timestamp 263 262 alter table comments add column edited text; -- timestamp 263 + `) 264 + return err 265 + }) 266 + 267 + runMigration(db, "add-source-info-to-pulls", func(tx *sql.Tx) error { 268 + _, err := tx.Exec(` 269 + alter table pulls add column source_branch text; 270 + alter table pulls add column source_repo_at text; 264 271 `) 265 272 return err 266 273 })
+82 -7
appview/db/pulls.go
··· 62 62 Submissions []*PullSubmission 63 63 64 64 // meta 65 - Created time.Time 65 + Created time.Time 66 + PullSource *PullSource 67 + } 68 + 69 + type PullSource struct { 70 + Branch string 71 + Repo *syntax.ATURI 66 72 } 67 73 68 74 type PullSubmission struct { ··· 107 113 108 114 func (p *Pull) LastRoundNumber() int { 109 115 return len(p.Submissions) - 1 116 + } 117 + 118 + func (p *Pull) IsSameRepoBranch() bool { 119 + if p.PullSource != nil { 120 + if p.PullSource.Repo != nil { 121 + return p.PullSource.Repo == &p.RepoAt 122 + } else { 123 + // no repo specified 124 + return true 125 + } 126 + } 127 + return false 110 128 } 111 129 112 130 func (s PullSubmission) AsNiceDiff(targetBranch string) types.NiceDiff { ··· 175 193 pull.PullId = nextId 176 194 pull.State = PullOpen 177 195 178 - _, err = tx.Exec(` 179 - insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state) 180 - values (?, ?, ?, ?, ?, ?, ?, ?) 181 - `, pull.RepoAt, pull.OwnerDid, pull.PullId, pull.Title, pull.TargetBranch, pull.Body, pull.Rkey, pull.State) 196 + var sourceBranch, sourceRepoAt *string 197 + if pull.PullSource != nil { 198 + sourceBranch = &pull.PullSource.Branch 199 + if pull.PullSource.Repo != nil { 200 + x := pull.PullSource.Repo.String() 201 + sourceRepoAt = &x 202 + } 203 + } 204 + 205 + _, err = tx.Exec( 206 + ` 207 + insert into pulls (repo_at, owner_did, pull_id, title, target_branch, body, rkey, state, source_branch, source_repo_at) 208 + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 209 + pull.RepoAt, 210 + pull.OwnerDid, 211 + pull.PullId, 212 + pull.Title, 213 + pull.TargetBranch, 214 + pull.Body, 215 + pull.Rkey, 216 + pull.State, 217 + sourceBranch, 218 + sourceRepoAt, 219 + ) 182 220 if err != nil { 183 221 return err 184 222 } ··· 228 266 target_branch, 229 267 pull_at, 230 268 body, 231 - rkey 269 + rkey, 270 + source_branch, 271 + source_repo_at 232 272 from 233 273 pulls 234 274 where ··· 243 283 for rows.Next() { 244 284 var pull Pull 245 285 var createdAt string 286 + var sourceBranch, sourceRepoAt sql.NullString 246 287 err := rows.Scan( 247 288 &pull.OwnerDid, 248 289 &pull.PullId, ··· 253 294 &pull.PullAt, 254 295 &pull.Body, 255 296 &pull.Rkey, 297 + &sourceBranch, 298 + &sourceRepoAt, 256 299 ) 257 300 if err != nil { 258 301 return nil, err ··· 264 307 } 265 308 pull.Created = createdTime 266 309 310 + if sourceBranch.Valid { 311 + pull.PullSource = &PullSource{ 312 + Branch: sourceBranch.String, 313 + } 314 + if sourceRepoAt.Valid { 315 + sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String) 316 + if err != nil { 317 + return nil, err 318 + } 319 + pull.PullSource.Repo = &sourceRepoAtParsed 320 + } 321 + } 322 + 267 323 pulls = append(pulls, pull) 268 324 } 269 325 ··· 286 342 pull_at, 287 343 repo_at, 288 344 body, 289 - rkey 345 + rkey, 346 + source_branch, 347 + source_repo_at 290 348 from 291 349 pulls 292 350 where ··· 296 354 297 355 var pull Pull 298 356 var createdAt string 357 + var sourceBranch, sourceRepoAt sql.NullString 299 358 err := row.Scan( 300 359 &pull.OwnerDid, 301 360 &pull.PullId, ··· 307 366 &pull.RepoAt, 308 367 &pull.Body, 309 368 &pull.Rkey, 369 + &sourceBranch, 370 + &sourceRepoAt, 310 371 ) 311 372 if err != nil { 312 373 return nil, err ··· 317 378 return nil, err 318 379 } 319 380 pull.Created = createdTime 381 + 382 + // populate source 383 + if sourceBranch.Valid { 384 + pull.PullSource = &PullSource{ 385 + Branch: sourceBranch.String, 386 + } 387 + if sourceRepoAt.Valid { 388 + sourceRepoAtParsed, err := syntax.ParseATURI(sourceRepoAt.String) 389 + if err != nil { 390 + return nil, err 391 + } 392 + pull.PullSource.Repo = &sourceRepoAtParsed 393 + } 394 + } 320 395 321 396 submissionsQuery := ` 322 397 select
+2 -3
appview/pages/pages.go
··· 591 591 RepoInfo RepoInfo 592 592 Active string 593 593 DidHandleMap map[string]string 594 - 595 - Pull db.Pull 596 - MergeCheck types.MergeCheckResponse 594 + Pull *db.Pull 595 + MergeCheck types.MergeCheckResponse 597 596 } 598 597 599 598 func (p *Pages) RepoSinglePull(w io.Writer, params RepoSinglePullParams) error {
+15 -8
appview/pages/templates/fragments/pullActions.html
··· 9 9 {{ $isConflicted := and .MergeCheck (or .MergeCheck.Error .MergeCheck.IsConflicted) }} 10 10 {{ $isPullAuthor := and .LoggedInUser (eq .LoggedInUser.Did .Pull.OwnerDid) }} 11 11 {{ $isLastRound := eq $roundNumber $lastIdx }} 12 + {{ $isSameRepoBranch := .Pull.IsSameRepoBranch }} 12 13 <div class="relative w-fit"> 13 14 <div class="absolute left-8 -top-2 w-px h-2 bg-gray-300 dark:bg-gray-600"></div> 14 15 <div id="actions-{{$roundNumber}}" class="flex flex-wrap gap-2"> ··· 36 37 {{ end }} 37 38 38 39 {{ if and $isPullAuthor $isOpen $isLastRound }} 39 - <button 40 - hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 41 - hx-target="#actions-{{$roundNumber}}" 42 - hx-swap="outerHtml" 43 - class="btn p-2 flex items-center gap-2"> 44 - {{ i "rotate-ccw" "w-4 h-4" }} 45 - <span>resubmit</span> 46 - </button> 40 + <button id="resubmitBtn" 41 + {{ if $isSameRepoBranch }} 42 + hx-post="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 43 + {{ else }} 44 + hx-get="/{{ .RepoInfo.FullName }}/pulls/{{ .Pull.PullId }}/resubmit" 45 + hx-target="#actions-{{$roundNumber}}" 46 + hx-swap="outerHtml" 47 + {{ end }} 48 + 49 + hx-disabled-elt="#resubmitBtn" 50 + class="btn p-2 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"> 51 + {{ i "rotate-ccw" "w-4 h-4" }} 52 + <span>resubmit</span> 53 + </button> 47 54 {{ end }} 48 55 49 56 {{ if and (or $isPullAuthor $isPushAllowed) $isOpen $isLastRound }}
+73 -39
appview/pages/templates/repo/pulls/new.html
··· 8 8 <ul class="list-decimal pl-10 space-y-2 text-gray-700 dark:text-gray-300"> 9 9 <li class="leading-relaxed">Clone this repository.</li> 10 10 <li class="leading-relaxed">Make your changes in your local repository.</li> 11 - <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> 11 + <li class="leading-relaxed">Grab the diff using <code>git diff</code>.</li> 12 12 <li class="leading-relaxed">Paste the diff output in the form below.</li> 13 13 </ul> 14 14 </p> ··· 19 19 hx-swap="none" 20 20 > 21 21 <div class="flex flex-col gap-4"> 22 - <div> 23 - <label for="title" class="dark:text-white">write a title</label> 24 - <input type="text" name="title" id="title" class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600" /> 22 + <div> 23 + <label for="title" class="dark:text-white">write a title</label> 24 + <input type="text" name="title" id="title" class="w-full dark:bg-gray-700 dark:text-white dark:border-gray-600" /> 25 + </div> 25 26 26 - <label for="targetBranch" class="dark:text-white">select a target branch</label> 27 - <p class="text-gray-500 dark:text-gray-400"> 28 - The branch you want to make your change against. 29 - </p> 30 - <select 31 - name="targetBranch" 32 - class="p-1 mb-2 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600" 33 - > 34 - <option disabled selected>select a branch</option> 35 - {{ range .Branches }} 36 - <option value="{{ .Reference.Name }}" class="py-1"> 37 - {{ .Reference.Name }} 38 - </option> 39 - {{ end }} 40 - </select> 41 - <label for="body" class="dark:text-white">add a description</label> 42 - <textarea 43 - name="body" 44 - id="body" 45 - rows="6" 46 - class="w-full resize-y dark:bg-gray-700 dark:text-white dark:border-gray-600" 47 - placeholder="Describe your change. Markdown is supported." 27 + <div> 28 + <label for="body" class="dark:text-white">add a description</label> 29 + <textarea 30 + name="body" 31 + id="body" 32 + rows="6" 33 + class="w-full resize-y dark:bg-gray-700 dark:text-white dark:border-gray-600" 34 + placeholder="Describe your change. Markdown is supported." 48 35 ></textarea> 36 + </div> 49 37 50 - <div class="mt-4"> 51 - <label for="patch" class="dark:text-white">paste your patch here</label> 52 - <textarea 53 - name="patch" 54 - id="patch" 55 - rows="10" 56 - class="w-full resize-y font-mono dark:bg-gray-700 dark:text-white dark:border-gray-600" 57 - placeholder="Paste your git diff output here." 58 - ></textarea> 59 - </div> 60 - </div> 61 - <div> 62 - <button type="submit" class="btn dark:bg-gray-600 dark:hover:bg-gray-500 dark:text-white">create</button> 38 + <div> 39 + <label for="targetBranch" class="dark:text-white">configure branches</label> 40 + <div class="flex flex-wrap gap-2 items-center"> 41 + <select 42 + required 43 + name="targetBranch" 44 + class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600" 45 + > 46 + <option disabled selected>target branch</option> 47 + {{ range .Branches }} 48 + <option value="{{ .Reference.Name }}" class="py-1"> 49 + {{ .Reference.Name }} 50 + </option> 51 + {{ end }} 52 + </select> 53 + 54 + {{ if .RepoInfo.Roles.IsPushAllowed }} 55 + {{ i "move-left" "w-5 h-5" }} 56 + <select 57 + name="sourceBranch" 58 + class="p-1 border border-gray-200 bg-white dark:bg-gray-700 dark:text-white dark:border-gray-600" 59 + > 60 + <option disabled selected>source branch</option> 61 + {{ range .Branches }} 62 + <option value="{{ .Reference.Name }}" class="py-1"> 63 + {{ .Reference.Name }} 64 + </option> 65 + {{ end }} 66 + </select> 67 + {{ end }} 68 + 63 69 </div> 70 + </div> 71 + 72 + <div class="mt-4"> 73 + {{ $label := "paste your patch here" }} 74 + {{ $rows := 10 }} 75 + {{ if .RepoInfo.Roles.IsPushAllowed }} 76 + {{ $label = "or paste your patch here" }} 77 + {{ $rows = 4 }} 78 + {{ end }} 79 + 80 + <label for="patch" class="dark:text-white">{{ $label }}</label> 81 + <textarea 82 + name="patch" 83 + id="patch" 84 + rows="{{$rows}}" 85 + class="w-full resize-y font-mono dark:bg-gray-700 dark:text-white dark:border-gray-600" 86 + placeholder="Paste your git diff output here." 87 + ></textarea> 88 + </div> 89 + 90 + <div class="flex justify-end items-center gap-2"> 91 + <button type="submit" class="btn">create</button> 92 + </div> 93 + 64 94 </div> 65 95 <div id="pull" class="error dark:text-red-300"></div> 66 96 </form> 67 97 {{ end }} 98 + 99 + {{ define "repoAfter" }} 100 + <div id="patch-preview" class="error dark:text-red-300"></div> 101 + {{ end }}
+9 -1
appview/pages/templates/repo/pulls/pull.html
··· 39 39 <span class="select-none before:content-['\00B7']"></span> 40 40 <time>{{ .Pull.Created | timeFmt }}</time> 41 41 <span class="select-none before:content-['\00B7']"></span> 42 - <span>targeting branch 42 + <span> 43 + targeting 43 44 <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"> 44 45 {{ .Pull.TargetBranch }} 45 46 </span> 46 47 </span> 48 + {{ if .Pull.IsSameRepoBranch }} 49 + <span>from 50 + <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"> 51 + {{ .Pull.PullSource.Branch }} 52 + </span> 53 + </span> 54 + {{ end }} 47 55 </span> 48 56 </div> 49 57
+8 -1
appview/pages/templates/repo/pulls/pulls.html
··· 73 73 </span> 74 74 75 75 <span class="before:content-['·']"> 76 - targeting branch 76 + targeting 77 77 <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"> 78 78 {{ .TargetBranch }} 79 79 </span> 80 80 </span> 81 + {{ if .IsSameRepoBranch }} 82 + <span>from 83 + <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"> 84 + {{ .PullSource.Branch }} 85 + </span> 86 + </span> 87 + {{ end }} 81 88 </p> 82 89 </div> 83 90 {{ end }}
+103 -3
appview/state/pull.go
··· 110 110 LoggedInUser: user, 111 111 RepoInfo: f.RepoInfo(s, user), 112 112 DidHandleMap: didHandleMap, 113 - Pull: *pull, 113 + Pull: pull, 114 114 MergeCheck: mergeCheckResponse, 115 115 }) 116 116 } ··· 450 450 Branches: result.Branches, 451 451 }) 452 452 case http.MethodPost: 453 + isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed() 453 454 title := r.FormValue("title") 454 455 body := r.FormValue("body") 455 456 targetBranch := r.FormValue("targetBranch") 457 + sourceBranch := r.FormValue("sourceBranch") 456 458 patch := r.FormValue("patch") 457 459 458 - if title == "" || body == "" || patch == "" || targetBranch == "" { 459 - s.pages.Notice(w, "pull", "Title, body and patch diff are required.") 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.") 460 466 return 461 467 } 462 468 469 + if patch != "" && sourceBranch != "" { 470 + s.pages.Notice(w, "pull", "Cannot select both patch and source branch.") 471 + return 472 + } 473 + 474 + if title == "" || body == "" || targetBranch == "" { 475 + s.pages.Notice(w, "pull", "Title, body and target branch are required.") 476 + return 477 + } 478 + 479 + // TODO: check if knot has this capability 480 + var pullSource *db.PullSource 481 + if sourceBranch != "" && isPushAllowed { 482 + pullSource = &db.PullSource{ 483 + Branch: sourceBranch, 484 + } 485 + // generate a patch using /compare 486 + ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 487 + if err != nil { 488 + log.Printf("failed to create signed client for %s: %s", f.Knot, err) 489 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 490 + return 491 + } 492 + 493 + log.Println(targetBranch, sourceBranch) 494 + 495 + resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 496 + switch resp.StatusCode { 497 + case 404: 498 + case 400: 499 + s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 500 + } 501 + 502 + respBody, err := io.ReadAll(resp.Body) 503 + if err != nil { 504 + log.Println("failed to compare across branches") 505 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 506 + } 507 + defer resp.Body.Close() 508 + 509 + var diffTreeResponse types.RepoDiffTreeResponse 510 + err = json.Unmarshal(respBody, &diffTreeResponse) 511 + if err != nil { 512 + log.Println("failed to unmarshal diff tree response", err) 513 + log.Println(string(respBody)) 514 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 515 + } 516 + 517 + patch = diffTreeResponse.DiffTree.Patch 518 + } 519 + 520 + log.Println(patch) 521 + 463 522 // Validate patch format 464 523 if !isPatchValid(patch) { 465 524 s.pages.Notice(w, "pull", "Invalid patch format. Please provide a valid diff.") ··· 488 547 Submissions: []*db.PullSubmission{ 489 548 &initialSubmission, 490 549 }, 550 + PullSource: pullSource, 491 551 }) 492 552 if err != nil { 493 553 log.Println("failed to create pull request", err) ··· 553 613 return 554 614 case http.MethodPost: 555 615 patch := r.FormValue("patch") 616 + 617 + // this pull is a branch based pull 618 + isPushAllowed := f.RepoInfo(s, user).Roles.IsPushAllowed() 619 + if pull.IsSameRepoBranch() && isPushAllowed { 620 + sourceBranch := pull.PullSource.Branch 621 + targetBranch := pull.TargetBranch 622 + // extract patch by performing compare 623 + ksClient, err := NewUnsignedClient(f.Knot, s.config.Dev) 624 + if err != nil { 625 + log.Printf("failed to create signed client for %s: %s", f.Knot, err) 626 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 627 + return 628 + } 629 + 630 + log.Println(targetBranch, sourceBranch) 631 + 632 + resp, err := ksClient.Compare(f.OwnerDid(), f.RepoName, targetBranch, sourceBranch) 633 + switch resp.StatusCode { 634 + case 404: 635 + case 400: 636 + s.pages.Notice(w, "pull", "Branch based pull requests are not supported on this knot.") 637 + } 638 + 639 + respBody, err := io.ReadAll(resp.Body) 640 + if err != nil { 641 + log.Println("failed to compare across branches") 642 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 643 + } 644 + defer resp.Body.Close() 645 + 646 + var diffTreeResponse types.RepoDiffTreeResponse 647 + err = json.Unmarshal(respBody, &diffTreeResponse) 648 + if err != nil { 649 + log.Println("failed to unmarshal diff tree response", err) 650 + log.Println(string(respBody)) 651 + s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.") 652 + } 653 + 654 + patch = diffTreeResponse.DiffTree.Patch 655 + } 556 656 557 657 if patch == "" { 558 658 s.pages.Notice(w, "resubmit-error", "Patch is empty.")
+15
appview/state/signer.go
··· 315 315 316 316 return us.client.Do(req) 317 317 } 318 + 319 + func (us *UnsignedClient) Compare(ownerDid, repoName, rev1, rev2 string) (*http.Response, error) { 320 + const ( 321 + Method = "GET" 322 + ) 323 + 324 + endpoint := fmt.Sprintf("/%s/%s/compare/%s/%s", ownerDid, repoName, rev1, rev2) 325 + 326 + req, err := us.newRequest(Method, endpoint, nil) 327 + if err != nil { 328 + return nil, err 329 + } 330 + 331 + return us.client.Do(req) 332 + }
+1
cmd/gen.go
··· 23 23 shtangled.RepoIssue{}, 24 24 shtangled.Repo{}, 25 25 shtangled.RepoPull{}, 26 + shtangled.RepoPull_Source{}, 26 27 shtangled.RepoPullStatus{}, 27 28 shtangled.RepoPullComment{}, 28 29 ); err != nil {
+71 -10
knotserver/git/diff.go
··· 6 6 "strings" 7 7 8 8 "github.com/bluekeyes/go-gitdiff/gitdiff" 9 + "github.com/go-git/go-git/v5/plumbing" 9 10 "github.com/go-git/go-git/v5/plumbing/object" 10 11 "tangled.sh/tangled.sh/core/types" 11 12 ) ··· 46 47 } 47 48 48 49 nd := types.NiceDiff{} 49 - nd.Commit.This = c.Hash.String() 50 - 51 - if parent.Hash.IsZero() { 52 - nd.Commit.Parent = "" 53 - } else { 54 - nd.Commit.Parent = parent.Hash.String() 55 - } 56 - nd.Commit.Author = c.Author 57 - nd.Commit.Message = c.Message 58 - 59 50 for _, d := range diffs { 60 51 ndiff := types.Diff{} 61 52 ndiff.Name.New = d.NewName ··· 82 73 } 83 74 84 75 nd.Stat.FilesChanged = len(diffs) 76 + nd.Commit.This = c.Hash.String() 77 + 78 + if parent.Hash.IsZero() { 79 + nd.Commit.Parent = "" 80 + } else { 81 + nd.Commit.Parent = parent.Hash.String() 82 + } 83 + nd.Commit.Author = c.Author 84 + nd.Commit.Message = c.Message 85 85 86 86 return &nd, nil 87 87 } 88 + 89 + func (g *GitRepo) DiffTree(rev1, rev2 string) (*types.DiffTree, error) { 90 + commit1, err := g.resolveRevision(rev1) 91 + if err != nil { 92 + return nil, fmt.Errorf("Invalid revision: %s", rev1) 93 + } 94 + 95 + commit2, err := g.resolveRevision(rev2) 96 + if err != nil { 97 + return nil, fmt.Errorf("Invalid revision: %s", rev2) 98 + } 99 + 100 + log.Println(commit1, commit2) 101 + 102 + tree1, err := commit1.Tree() 103 + if err != nil { 104 + return nil, err 105 + } 106 + 107 + tree2, err := commit2.Tree() 108 + if err != nil { 109 + return nil, err 110 + } 111 + 112 + diff, err := object.DiffTree(tree1, tree2) 113 + if err != nil { 114 + return nil, err 115 + } 116 + 117 + patch, err := diff.Patch() 118 + if err != nil { 119 + return nil, err 120 + } 121 + 122 + diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String())) 123 + if err != nil { 124 + return nil, err 125 + } 126 + 127 + return &types.DiffTree{ 128 + Rev1: commit1.Hash.String(), 129 + Rev2: commit2.Hash.String(), 130 + Patch: patch.String(), 131 + Diff: diffs, 132 + }, nil 133 + } 134 + 135 + func (g *GitRepo) resolveRevision(revStr string) (*object.Commit, error) { 136 + rev, err := g.r.ResolveRevision(plumbing.Revision(revStr)) 137 + if err != nil { 138 + return nil, fmt.Errorf("resolving revision %s: %w", revStr, err) 139 + } 140 + 141 + commit, err := g.r.CommitObject(*rev) 142 + if err != nil { 143 + 144 + return nil, fmt.Errorf("getting commit for %s: %w", revStr, err) 145 + } 146 + 147 + return commit, nil 148 + }
+10
knotserver/git/git.go
··· 131 131 return &g, nil 132 132 } 133 133 134 + func PlainOpen(path string) (*GitRepo, error) { 135 + var err error 136 + g := GitRepo{path: path} 137 + g.r, err = git.PlainOpen(path) 138 + if err != nil { 139 + return nil, fmt.Errorf("opening %s: %w", path, err) 140 + } 141 + return &g, nil 142 + } 143 + 134 144 func (g *GitRepo) Commits() ([]*object.Commit, error) { 135 145 ci, err := g.r.Log(&git.LogOptions{From: g.h}) 136 146 if err != nil {
+1
knotserver/handler.go
··· 83 83 r.Get("/", h.RepoIndex) 84 84 r.Get("/info/refs", h.InfoRefs) 85 85 r.Post("/git-upload-pack", h.UploadPack) 86 + r.Get("/compare/{rev1}/{rev2}", h.Compare) // git diff-tree compare of two objects 86 87 87 88 r.Route("/merge", func(r chi.Router) { 88 89 r.With(h.VerifySignature)
+30 -1
knotserver/routes.go
··· 36 36 37 37 capabilities := map[string]any{ 38 38 "pull_requests": map[string]any{ 39 - "patch_submissions": true, 39 + "patch_submissions": true, 40 + "branch_submissions": true, 41 + "fork_submissions": false, 40 42 }, 41 43 } 42 44 ··· 681 683 } 682 684 writeError(w, err.Error(), http.StatusInternalServerError) 683 685 h.l.Error("git: failed to check merge", "handler", "MergeCheck", "error", err.Error()) 686 + } 687 + 688 + func (h *Handle) Compare(w http.ResponseWriter, r *http.Request) { 689 + rev1 := chi.URLParam(r, "rev1") 690 + rev1, _ = url.PathUnescape(rev1) 691 + 692 + rev2 := chi.URLParam(r, "rev2") 693 + rev2, _ = url.PathUnescape(rev2) 694 + 695 + l := h.l.With("handler", "Compare", "r1", rev1, "r2", rev2) 696 + 697 + path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r)) 698 + gr, err := git.PlainOpen(path) 699 + if err != nil { 700 + notFound(w) 701 + return 702 + } 703 + 704 + difftree, err := gr.DiffTree(rev1, rev2) 705 + if err != nil { 706 + l.Error("error comparing revisions", "msg", err.Error()) 707 + writeError(w, "error comparing revisions", http.StatusBadRequest) 708 + return 709 + } 710 + 711 + writeJSON(w, types.RepoDiffTreeResponse{difftree}) 712 + return 684 713 } 685 714 686 715 func (h *Handle) AddMember(w http.ResponseWriter, r *http.Request) {
+24 -5
lexicons/pulls/pull.json
··· 9 9 "key": "tid", 10 10 "record": { 11 11 "type": "object", 12 - "required": ["targetRepo", "targetBranch", "pullId", "title", "patch"], 12 + "required": [ 13 + "targetRepo", 14 + "targetBranch", 15 + "pullId", 16 + "title", 17 + "patch" 18 + ], 13 19 "properties": { 14 20 "targetRepo": { 15 21 "type": "string", ··· 18 24 "targetBranch": { 19 25 "type": "string" 20 26 }, 21 - "sourceRepo": { 22 - "type": "string", 23 - "format": "at-uri" 24 - }, 25 27 "pullId": { 26 28 "type": "integer" 27 29 }, ··· 37 39 }, 38 40 "patch": { 39 41 "type": "string" 42 + }, 43 + "source": { 44 + "type": "ref", 45 + "ref": "#source" 40 46 } 47 + } 48 + } 49 + }, 50 + "source": { 51 + "type": "object", 52 + "required": ["branch"], 53 + "properties": { 54 + "branch": { 55 + "type": "string" 56 + }, 57 + "repo": { 58 + "type": "string", 59 + "format": "at-uri" 41 60 } 42 61 } 43 62 }
+7
types/diff.go
··· 38 38 } `json:"stat"` 39 39 Diff []Diff `json:"diff"` 40 40 } 41 + 42 + type DiffTree struct { 43 + Rev1 string `json:"rev1"` 44 + Rev2 string `json:"rev2"` 45 + Patch string `json:"patch"` 46 + Diff []*gitdiff.File `json:"diff"` 47 + }
+4
types/repo.go
··· 32 32 Diff *NiceDiff `json:"diff,omitempty"` 33 33 } 34 34 35 + type RepoDiffTreeResponse struct { 36 + DiffTree *DiffTree `json:"difftree,omitempty"` 37 + } 38 + 35 39 type RepoTreeResponse struct { 36 40 Ref string `json:"ref,omitempty"` 37 41 Parent string `json:"parent,omitempty"`