| 1 | --- |
| 2 | title: Pull requests |
| 3 | description: Server-side merge base, tree diffs, and fast-forward or three-way merges, computed inside the repository. |
| 4 | --- |
| 5 | |
| 6 | Pull requests are computed where the objects are. Merge base, diffs, |
| 7 | and the merge itself run inside the repository's Durable Object, so |
| 8 | nothing clones a history to answer what changed. |
| 9 | |
| 10 | ## Opening |
| 11 | |
| 12 | ```typescript |
| 13 | const pull = yield* client.pulls.create({ |
| 14 | params: { owner: "acme", repo: "web" }, |
| 15 | payload: { |
| 16 | title: "Add the pricing page", |
| 17 | body: "Closes the last gap in the marketing site.", |
| 18 | base: "main", |
| 19 | head: "pricing", |
| 20 | }, |
| 21 | }); |
| 22 | ``` |
| 23 | |
| 24 | Branch names may be short or fully qualified. Numbers are per |
| 25 | repository and start at 1. A missing branch, or `base` equal to |
| 26 | `head`, is a typed failure. |
| 27 | |
| 28 | ## Listing and reading |
| 29 | |
| 30 | ```typescript |
| 31 | const page = yield* client.pulls.list({ |
| 32 | params: { owner: "acme", repo: "web" }, |
| 33 | query: { state: "open" }, // "open" | "closed" | "merged" | "all" |
| 34 | }); |
| 35 | ``` |
| 36 | |
| 37 | Listing is keyset-paginated, newest first. Reading one pull request |
| 38 | computes its state live: merge base, ahead and behind counts, and the |
| 39 | changed files, as the branches are now. If the head branch is deleted |
| 40 | while it is open, the comparison fields read as null. |
| 41 | |
| 42 | ## Diffs |
| 43 | |
| 44 | The same machinery works without a pull request: |
| 45 | |
| 46 | ```typescript |
| 47 | // what one commit changed |
| 48 | yield* client.objects.commitDiff({ |
| 49 | params: { owner: "acme", repo: "web", oid }, |
| 50 | }); |
| 51 | |
| 52 | // three-dot comparison between two refs |
| 53 | yield* client.objects.compare({ |
| 54 | params: { owner: "acme", repo: "web" }, |
| 55 | query: { base: "main", head: "pricing" }, |
| 56 | }); |
| 57 | ``` |
| 58 | |
| 59 | Comparison finds the merge base in the commit graph and diffs the two |
| 60 | trees. Identical subtrees are pruned on entry, so the cost tracks what |
| 61 | changed rather than the size of the repository. |
| 62 | |
| 63 | ## Merging |
| 64 | |
| 65 | ```typescript |
| 66 | yield* client.pulls.merge({ |
| 67 | params: { owner: "acme", repo: "web", number: 1 }, |
| 68 | payload: { message: "Add the pricing page (#1)" }, |
| 69 | }); |
| 70 | ``` |
| 71 | |
| 72 | The strategy is chosen for you. When the base has not moved, the ref |
| 73 | fast-forwards and `message` is ignored. Otherwise, if the two sides |
| 74 | changed disjoint files, a merge commit is written with both parents. |
| 75 | |
| 76 | Overlapping edits to one file are a typed `409`. The host does not |
| 77 | merge hunks or write conflict markers. Resolve locally and push. |
| 78 | |
| 79 | Merging is terminal. A merged pull request cannot be reopened, and |
| 80 | merging it twice is a typed conflict. |
| 81 | |
| 82 | ## Races |
| 83 | |
| 84 | Pass `expectedHeadOid` so a branch that moved between the review and |
| 85 | the click is refused rather than merged: |
| 86 | |
| 87 | ```typescript |
| 88 | yield* client.pulls.merge({ |
| 89 | params: { owner: "acme", repo: "web", number: 1 }, |
| 90 | payload: { expectedHeadOid: headOidTheUserSaw }, |
| 91 | }); |
| 92 | ``` |
| 93 | |
| 94 | ## Not included |
| 95 | |
| 96 | Reviews, comments, approvals, and required checks. A pull request here |
| 97 | is a branch comparison with a title and a merge, and the review |
| 98 | workflow is yours to build with these endpoints. |
| 99 | [Part 4 of the tutorial](/git/tutorial/part-4) starts there. |