g1t is the stored author of what it opens; the person who asked is requested_by and keeps the author's rights
| 752 | 752 | "The check names reported on a repository's commits in the last 30 days, most recent first, with the events each was reported for: the names update_repo_settings takes in required_checks. A workflow's runs report a check named after the workflow; a check required on the default branch must be reported on a pull request's head (pull_request events) and, with the merge queue on, on its queued state (merge_group events)." | |
| 753 | 753 | } | |
| 754 | 754 | Op::MessageAgent => { | |
| 755 | − | "Send the agent working on a pull request a message: a correction, a hint, a change of plan. It receives it at its next step, and it is recorded in the pull request's session. The pull request's author, and anyone with the Write role or higher. An agent uses it to ask the agent on another pull request a question (kind: question) or hand it work that belongs there (kind: handoff), giving its own pull request as from_number; the answer comes back to it at its next step." | |
| 755 | + | "Send the agent working on a pull request a message: a correction, a hint, a change of plan. It receives it at its next step, and it is recorded in the pull request's session. The pull request's author (for one g1t made, whoever asked for it), and anyone with the Write role or higher. An agent uses it to ask the agent on another pull request a question (kind: question) or hand it work that belongs there (kind: handoff), giving its own pull request as from_number; the answer comes back to it at its next step." | |
| 756 | 756 | } | |
| 757 | 757 | Op::AnswerMessage => { | |
| 758 | 758 | "Answer a question or a handoff another agent sent you, by the message's id. For a handoff, set decline to say it is not yours to take. The answer reaches the asking agent at its next step." | |
| 791 | 791 | "Open an issue on a repository. Say what done means in the body if it helps, for instance under a \"Definition of done\" heading; what must pass before a pull request for it merges is the default branch's required checks, the same for every pull request." | |
| 792 | 792 | } | |
| 793 | 793 | Op::UpdateIssue => { | |
| 794 | − | "Change an issue's title, body, labels or the people it is assigned to. Only the fields given are changed; labels and assignees each replace the whole set. Its author may change their own issue; anyone else needs the Triage role or higher." | |
| 794 | + | "Change an issue's title, body, labels or the people it is assigned to. Only the fields given are changed; labels and assignees each replace the whole set. Its author may change their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher." | |
| 795 | 795 | } | |
| 796 | 796 | Op::CloseIssue => { | |
| 797 | − | "Close an issue without a pull request. Merging a pull request made for an issue closes it for you. Its author may close their own issue; anyone else needs the Triage role or higher." | |
| 797 | + | "Close an issue without a pull request. Merging a pull request made for an issue closes it for you. Its author may close their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher." | |
| 798 | 798 | } | |
| 799 | − | Op::ReopenIssue => "Reopen a closed issue. Its author may reopen their own issue; anyone else needs the Triage role or higher.", | |
| 799 | + | Op::ReopenIssue => "Reopen a closed issue. Its author may reopen their own issue, as may the person g1t filed one for; anyone else needs the Triage role or higher.", | |
| 800 | 800 | Op::PlanWork => { | |
| 801 | 801 | "Turn an outcome into a plan. An agent reads the repository and proposes the issues that would get there: what each changes, what done means for it (added to its body under \"Definition of done\"), the files it will touch, and which must merge before which. Returns the plan's id at once; the plan takes a minute or two to write, so read it with get_plan until its status is ready. Nothing is opened until apply_plan. Needs the Write role or higher." | |
| 802 | 802 | } | |
| 807 | 807 | "Open a plan's issues, each blocked by the ones it depends on. With assign, g1t agents start at once on every issue that depends on nothing, working in parallel, and on the others as what they depend on merges. keep limits it to some of the proposed issues, by their positions counting from 1. A plan is applied once. Needs the Write role or higher." | |
| 808 | 808 | } | |
| 809 | 809 | Op::AssignIssue => { | |
| 810 | − | "Assign an issue to g1t. It opens a pull request for the issue in a sandbox of its own and sees it through: the repository's workflows run on it as its checks, a second agent reviews it, it revises when a check fails (reading the failing jobs' logs) or the review asks for changes, and it catches up when main moves. It is ready once the default branch's required checks pass and the review approves. Returns the pull request at once; follow its progress with get_pull_request. There is no model or agent count to choose. To put many agents to work, assign many issues. Needs the Write role or higher. In preview: only for accounts g1t agents are enabled for." | |
| 810 | + | "Assign an issue to g1t. It opens a pull request for the issue in a sandbox of its own and sees it through: the repository's workflows run on it as its checks, a second agent reviews it, it revises when a check fails (reading the failing jobs' logs) or the review asks for changes, and it catches up when main moves. It is ready once the default branch's required checks pass and the review approves. Returns the pull request at once, with g1t as its author and you as its requested_by; follow its progress with get_pull_request. There is no model or agent count to choose. To put many agents to work, assign many issues. Needs the Write role or higher. In preview: only for accounts g1t agents are enabled for." | |
| 811 | 811 | } | |
| 812 | 812 | Op::Delegate => { | |
| 813 | 813 | "Put an agent on something in one step: open an issue and assign it to g1t at once. Say what you want done in plain words, with what done means if you know it. What must pass before its pull request merges is the default branch's required checks. Needs the Write role or higher, and nothing is opened without it. The issue is opened whatever happens next: agent.status is started (pull is the draft pull request the agent opened; follow it with get_pull_request), queued (every agent slot of the workspace is busy; it starts by itself when one frees up) or not_started, with agent.code saying why (not_paid, trial_used, limit, paused, issue_cap, billing_unavailable or no_model), agent.message saying what to do, and agent.fix_url where. There is no model or agent count to choose." | |
| 817 | 817 | "Comment on an issue or a pull request. On a pull request, give path and line to comment on one line of the change." | |
| 818 | 818 | } | |
| 819 | 819 | Op::ReviewPullRequest => { | |
| 820 | − | "Give a verdict on a pull request: approve it, or request changes and say what. Read get_pull_request_changes first. You cannot review a pull request you opened." | |
| 820 | + | "Give a verdict on a pull request: approve it, or request changes and say what. Read get_pull_request_changes first. You cannot review a pull request you opened, or one g1t made for you (you are its requested_by)." | |
| 821 | 821 | } | |
| 822 | 822 | Op::ListPullRequests => { | |
| 823 | 823 | "Pull requests on a repository, newest first. State open covers drafts and those ready for review; closed covers merged and closed." | |
| 835 | 835 | Op::MarkPullRequestReady => { | |
| 836 | 836 | "Mark a draft pull request ready for review. Push your commits first. The summary becomes its description and should say what changed and why." | |
| 837 | 837 | } | |
| 838 | − | Op::ClosePullRequest => "Close a pull request without merging it. Its author may close their own; anyone else needs the Triage role or higher.", | |
| 838 | + | Op::ClosePullRequest => "Close a pull request without merging it. Its author may close their own, and whoever asked g1t for one may close that one; anyone else needs the Triage role or higher.", | |
| 839 | 839 | Op::GetPullRequestChanges => { | |
| 840 | 840 | "What a pull request changes: the files it touches and their line-by-line diff against the commit it started from. Use it to review a pull request or to compare several made for the same issue." | |
| 841 | 841 | } |
| 834 | 834 | "verified": false, | |
| 835 | 835 | "workspaces": [] | |
| 836 | 836 | }, | |
| 837 | + | "requested_by": null, | |
| 837 | 838 | "created_at": "2026-10-01T18:04:11.482Z", | |
| 838 | 839 | "updated_at": "2026-10-01T18:04:11.482Z", | |
| 839 | 840 | "closed_at": null, | |
| 874 | 875 | "verified": false, | |
| 875 | 876 | "workspaces": [] | |
| 876 | 877 | }, | |
| 878 | + | "requested_by": null, | |
| 877 | 879 | "created_at": "2026-10-01T18:04:11.482Z", | |
| 878 | 880 | "updated_at": "2026-10-01T18:04:11.482Z", | |
| 879 | 881 | "closed_at": null, | |
| 907 | 909 | "verified": false, | |
| 908 | 910 | "workspaces": [] | |
| 909 | 911 | }, | |
| 912 | + | "requested_by": null, | |
| 910 | 913 | "created_at": "2026-10-01T18:04:11.482Z", | |
| 911 | 914 | "updated_at": "2026-10-01T18:52:17.093Z", | |
| 912 | 915 | "closed_at": "2026-10-01T18:52:17.093Z", | |
| 950 | 953 | "verified": false, | |
| 951 | 954 | "workspaces": [] | |
| 952 | 955 | }, | |
| 956 | + | "requested_by": null, | |
| 953 | 957 | "created_at": "2026-10-01T18:20:02.117Z", | |
| 954 | 958 | "updated_at": "2026-10-01T18:20:02.117Z" | |
| 955 | 959 | }, | |
| 993 | 997 | "verified": false, | |
| 994 | 998 | "workspaces": [] | |
| 995 | 999 | }, | |
| 1000 | + | "requested_by": null, | |
| 996 | 1001 | "created_at": "2026-10-01T18:20:02.117Z", | |
| 997 | 1002 | "updated_at": "2026-10-01T18:52:17.093Z" | |
| 998 | 1003 | } | |
| 1048 | 1053 | "verified": false, | |
| 1049 | 1054 | "workspaces": [] | |
| 1050 | 1055 | }, | |
| 1056 | + | "requested_by": null, | |
| 1051 | 1057 | "created_at": "2026-10-01T18:04:11.482Z", | |
| 1052 | 1058 | "updated_at": "2026-10-01T18:09:47.305Z", | |
| 1053 | 1059 | "closed_at": null, | |
| 1084 | 1090 | "verified": false, | |
| 1085 | 1091 | "workspaces": [] | |
| 1086 | 1092 | }, | |
| 1093 | + | "requested_by": null, | |
| 1087 | 1094 | "created_at": "2026-10-01T18:04:11.482Z", | |
| 1088 | 1095 | "updated_at": "2026-10-01T19:30:00.214Z", | |
| 1089 | 1096 | "closed_at": "2026-10-01T19:30:00.214Z", | |
| 1115 | 1122 | "verified": false, | |
| 1116 | 1123 | "workspaces": [] | |
| 1117 | 1124 | }, | |
| 1125 | + | "requested_by": null, | |
| 1118 | 1126 | "created_at": "2026-10-01T18:04:11.482Z", | |
| 1119 | 1127 | "updated_at": "2026-10-01T19:41:52.830Z", | |
| 1120 | 1128 | "closed_at": null, | |
| 1156 | 1164 | "assignees": [], | |
| 1157 | 1165 | "reviewers": [], | |
| 1158 | 1166 | "author": { | |
| 1167 | + | "id": "usr_g1t_agent", | |
| 1168 | + | "username": "g1t", | |
| 1169 | + | "kind": "agent", | |
| 1170 | + | "verified": false, | |
| 1171 | + | "workspaces": [] | |
| 1172 | + | }, | |
| 1173 | + | "requested_by": { | |
| 1159 | 1174 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1160 | 1175 | "username": "syntaqx", | |
| 1161 | 1176 | "kind": "user", | |
| 1166 | 1181 | "updated_at": "2026-10-01T18:20:02.117Z", | |
| 1167 | 1182 | "confidence": null | |
| 1168 | 1183 | }, | |
| 1169 | − | "notes": "The response is the pull request g1t opened, still a draft. Follow it with [get a pull request](/reference/api/pull-requests/get-pull-request/): `lifecycle` says what the agent is doing." | |
| 1184 | + | "notes": "The response is the pull request g1t opened, still a draft. g1t is its `author` and you are its `requested_by`: you may manage it as if you had opened it, and cannot approve it yourself. Follow it with [get a pull request](/reference/api/pull-requests/get-pull-request/): `lifecycle` says what the agent is doing." | |
| 1170 | 1185 | }, | |
| 1171 | 1186 | "delegate": { | |
| 1172 | 1187 | "request": { | |
| 1191 | 1206 | "verified": false, | |
| 1192 | 1207 | "workspaces": [] | |
| 1193 | 1208 | }, | |
| 1209 | + | "requested_by": null, | |
| 1194 | 1210 | "created_at": "2026-10-05T14:02:11.204Z", | |
| 1195 | 1211 | "updated_at": "2026-10-05T14:02:11.204Z", | |
| 1196 | 1212 | "closed_at": null, | |
| 1227 | 1243 | "assignees": [], | |
| 1228 | 1244 | "reviewers": [], | |
| 1229 | 1245 | "author": { | |
| 1246 | + | "id": "usr_g1t_agent", | |
| 1247 | + | "username": "g1t", | |
| 1248 | + | "kind": "agent", | |
| 1249 | + | "verified": false, | |
| 1250 | + | "workspaces": [] | |
| 1251 | + | }, | |
| 1252 | + | "requested_by": { | |
| 1230 | 1253 | "id": "usr_01kkntcg1eeb98j62xjm7eh09p", | |
| 1231 | 1254 | "username": "syntaqx", | |
| 1232 | 1255 | "kind": "user", | |
| 1457 | 1480 | "verified": false, | |
| 1458 | 1481 | "workspaces": [] | |
| 1459 | 1482 | }, | |
| 1483 | + | "requested_by": null, | |
| 1460 | 1484 | "created_at": "2026-10-01T18:20:02.117Z", | |
| 1461 | 1485 | "updated_at": "2026-10-01T18:35:44.902Z" | |
| 1462 | 1486 | } | |
| 1501 | 1525 | "verified": false, | |
| 1502 | 1526 | "workspaces": [] | |
| 1503 | 1527 | }, | |
| 1528 | + | "requested_by": null, | |
| 1504 | 1529 | "created_at": "2026-10-01T18:20:02.117Z", | |
| 1505 | 1530 | "updated_at": "2026-10-01T18:20:02.117Z" | |
| 1506 | 1531 | }, | |
| 1554 | 1579 | "verified": false, | |
| 1555 | 1580 | "workspaces": [] | |
| 1556 | 1581 | }, | |
| 1582 | + | "requested_by": null, | |
| 1557 | 1583 | "created_at": "2026-10-01T18:20:02.117Z", | |
| 1558 | 1584 | "updated_at": "2026-10-01T18:35:44.902Z", | |
| 1559 | 1585 | "confidence": null | |
| 1577 | 1603 | "verified": false, | |
| 1578 | 1604 | "workspaces": [] | |
| 1579 | 1605 | }, | |
| 1606 | + | "requested_by": null, | |
| 1580 | 1607 | "created_at": "2026-10-01T18:04:11.482Z", | |
| 1581 | 1608 | "updated_at": "2026-10-01T18:20:02.117Z", | |
| 1582 | 1609 | "closed_at": null, | |
| 1735 | 1762 | "verified": false, | |
| 1736 | 1763 | "workspaces": [] | |
| 1737 | 1764 | }, | |
| 1765 | + | "requested_by": null, | |
| 1738 | 1766 | "created_at": "2026-10-01T18:20:02.117Z", | |
| 1739 | 1767 | "updated_at": "2026-10-01T18:33:10.398Z" | |
| 1740 | 1768 | } | |
| 1810 | 1838 | "verified": false, | |
| 1811 | 1839 | "workspaces": [] | |
| 1812 | 1840 | }, | |
| 1841 | + | "requested_by": null, | |
| 1813 | 1842 | "created_at": "2026-10-01T18:20:02.117Z", | |
| 1814 | 1843 | "updated_at": "2026-10-01T18:52:17.093Z" | |
| 1815 | 1844 | }, | |
| 1854 | 1883 | "verified": false, | |
| 1855 | 1884 | "workspaces": [] | |
| 1856 | 1885 | }, | |
| 1886 | + | "requested_by": null, | |
| 1857 | 1887 | "created_at": "2026-10-01T18:20:02.117Z", | |
| 1858 | 1888 | "updated_at": "2026-10-01T19:02:48.760Z" | |
| 1859 | 1889 | } | |
| 3645 | 3675 | "number": null, | |
| 3646 | 3676 | "state": null, | |
| 3647 | 3677 | "author": null, | |
| 3678 | + | "requested_by": null, | |
| 3648 | 3679 | "labels": [], | |
| 3649 | 3680 | "topics": [], | |
| 3650 | 3681 | "slug": null, | |
| 3654 | 3685 | ], | |
| 3655 | 3686 | "notes": [] | |
| 3656 | 3687 | }, | |
| 3657 | − | "notes": "`q` takes words, `\"exact phrases\"`, `-words` to leave out, and qualifiers: `repo:owner/name`, `org:` (or `workspace:`), `language:`, `path:` (a glob when it has `*`), `is:issue`, `is:pr`, `is:open`, `is:closed`, `is:merged`, `is:draft`, `is:public`, `is:private`, `author:` and `label:`; most can be left out with a leading `-`, as in `-label:wontfix`. `type` is `repositories`, `code`, `issues`, `pulls` or `people`; without it, `path:` means code, `is:pr` pull requests, `is:open`, `author:` or `label:` issues, and anything else repositories. `counts` says how many results each type has, up to 1,000. Each result's `snippet` (or, for code, each of its `lines`) is a list of parts, `highlight` true where the query matched. Code is searched on default branches and needs a word of three characters or more, unless the query names a `repo:`. Public content is returned to anyone, without a token; private content only to people who can read it (members of its workspace, and people given a role on the repository), checked when the search runs, so a repository made private, or a role taken away, stops appearing at once. A g1t agent's token can search too." | |
| 3688 | + | "notes": "`q` takes words, `\"exact phrases\"`, `-words` to leave out, and qualifiers: `repo:owner/name`, `org:` (or `workspace:`), `language:`, `path:` (a glob when it has `*`), `is:issue`, `is:pr`, `is:open`, `is:closed`, `is:merged`, `is:draft`, `is:public`, `is:private`, `author:` and `label:`; most can be left out with a leading `-`, as in `-label:wontfix`. `type` is `repositories`, `code`, `issues`, `pulls` or `people`; without it, `path:` means code, `is:pr` pull requests, `is:open`, `author:` or `label:` issues, and anything else repositories. `counts` says how many results each type has, up to 1,000. Each result's `snippet` (or, for code, each of its `lines`) is a list of parts, `highlight` true where the query matched. Code is searched on default branches and needs a word of three characters or more, unless the query names a `repo:`. Public content is returned to anyone, without a token; private content only to people who can read it (members of its workspace, and people given a role on the repository), checked when the search runs, so a repository made private, or a role taken away, stops appearing at once. A g1t agent's token can search too. An issue or pull request g1t opened has `author` `g1t` and `requested_by` the person it was for; `author:` matches the author." | |
| 3658 | 3689 | }, | |
| 3659 | 3690 | "search_context": { | |
| 3660 | 3691 | "params": { |
| 184 | 184 | marks it ready, which on g1t is when it first has code. Each head runs | |
| 185 | 185 | each workflow once. | |
| 186 | 186 | ||
| 187 | + | `github.event.pull_request` reads as it does on GitHub. For a pull request | |
| 188 | + | g1t made, `pull_request.user` is g1t (`login` `g1t`, `type` `Bot`), and | |
| 189 | + | `pull_request.requested_by` names the person who asked for it; it is `null` | |
| 190 | + | on anyone else's. `github.event.issue.requested_by` does the same for an | |
| 191 | + | issue g1t's agent filed. `sender` is whoever caused the event. | |
| 192 | + | ||
| 187 | 193 | ## Checks | |
| 188 | 194 | ||
| 189 | 195 | A pull request's checks are its workflows. Each workflow that runs on | |
| 255 | 261 | token for the run, with `GITHUB_TOKEN` as its alias. A pull request's runs | |
| 256 | 262 | get secrets and the token only when its author has the Write | |
| 257 | 263 | [role](/guides/access-and-roles/) or higher on the repository, a member or | |
| 258 | − | an outside collaborator, or is g1t's agent. Anyone else's, such as one | |
| 264 | + | an outside collaborator, or is g1t working on its own. For a pull request | |
| 265 | + | g1t made, its author is g1t and the person who asked for it is the one | |
| 266 | + | whose role counts. Anyone else's, such as one | |
| 259 | 267 | from a fork or by someone with Read or Triage, runs without secrets and | |
| 260 | 268 | with an empty token. See | |
| 261 | 269 | [who gets secrets](/guides/secrets-and-variables/#who-gets-secrets). |
| 314 | 314 | rather than the build. Secrets reach a preview only when the pull | |
| 315 | 315 | request's author has Write or higher on the repository, whether a member | |
| 316 | 316 | or an [outside collaborator](/guides/access-and-roles/#outside-collaborators), | |
| 317 | − | or it is g1t's own agent. A preview of anyone else's pull request, such as | |
| 317 | + | or it is g1t working on its own. For a pull request g1t made, whoever | |
| 318 | + | asked for it is the one whose role counts. A preview of anyone else's pull request, such as | |
| 318 | 319 | one from a fork or by someone with Read or Triage, is built and runs with | |
| 319 | 320 | variables only, no secrets. | |
| 320 | 321 |
| 116 | 116 | https://g1t.sh/pulls/<pull request id>.git | |
| 117 | 117 | ``` | |
| 118 | 118 | ||
| 119 | − | Only whoever opened the pull request can push to it. Pushes to a fork | |
| 119 | + | Only whoever opened the pull request can push to it, or, for one g1t | |
| 120 | + | made, whoever asked for it. Pushes to a fork | |
| 120 | 121 | update the pull request's head commit on its page. | |
| 121 | 122 | ||
| 122 | 123 | ## Limits |
| 8 | 8 | whether it merges cleanly, and the button that merges it. Everything in it | |
| 9 | 9 | updates by itself while something is still running. | |
| 10 | 10 | ||
| 11 | + | A pull request g1t made shows **g1t** as its author and **requested by** | |
| 12 | + | the person who asked for it. That person can manage it as its author could, | |
| 13 | + | and cannot approve it; see | |
| 14 | + | [who a pull request is for](/guides/working-with-g1t/#who-a-pull-request-is-for). | |
| 15 | + | ||
| 11 | 16 | ## Checks | |
| 12 | 17 | ||
| 13 | 18 | A pull request's checks are the statuses reported on its head commit. Most | |
| 149 | 154 | - **Resolve with g1t.** g1t merges the target branch in, | |
| 150 | 155 | resolves the conflicts keeping what both sides meant, and pushes the | |
| 151 | 156 | result. It is told which files conflict. Available to whoever can push to | |
| 152 | − | the pull request: for a pull request's fork, whoever opened it; for a | |
| 153 | − | branch, anyone with the Write role or higher. | |
| 157 | + | the pull request: for a pull request's fork, whoever opened it (whoever | |
| 158 | + | asked g1t for one it made); for a branch, anyone with the Write role or | |
| 159 | + | higher. | |
| 154 | 160 | - **Resolve in the browser.** Coming soon. | |
| 155 | 161 | - **On the command line.** The box lists the commands, each with a copy | |
| 156 | 162 | button. For a pull request from a branch: | |
| 184 | 190 | ||
| 185 | 191 | When the target branch has moved, the merge box says **main has moved since | |
| 186 | 192 | this was made**. Whoever can push to the pull request (whoever opened it, | |
| 187 | − | for one in its own fork; anyone with the Write [role](/guides/access-and-roles/) or higher, | |
| 188 | − | for a branch) can | |
| 193 | + | or asked g1t for it, for one in its own fork; anyone with the Write | |
| 194 | + | [role](/guides/access-and-roles/) or higher, for a branch) can | |
| 189 | 195 | press **Catch up with main now**: | |
| 190 | 196 | ||
| 191 | 197 | 1. **When the two changed different files**, g1t merges `main` in itself, |
| 70 | 70 | | `is:open`, `is:closed` | Issues and pull requests by state | Issues, pull requests | | |
| 71 | 71 | | `is:merged`, `is:draft` | Pull requests that were merged, or are still drafts | Pull requests | | |
| 72 | 72 | | `is:public`, `is:private` | Results from public or private repositories | Repositories, code, issues, pull requests | | |
| 73 | − | | `author:ana` | Opened by someone | Issues, pull requests | | |
| 73 | + | | `author:ana` | Opened by someone. What g1t opened is `author:g1t`, and shows who it was for | Issues, pull requests | | |
| 74 | 74 | | `label:bug` | With a label. Quote a label with spaces: `label:"good first issue"` | Issues, pull requests | | |
| 75 | 75 | | `type:code` | Which tab to open: `repositories`, `code`, `issues`, `pulls` or `people` | All | | |
| 76 | 76 |
| 78 | 78 | - pushes, schedules, manual runs and the merge queue; | |
| 79 | 79 | - pull requests whose author has the Write [role](/guides/access-and-roles/) | |
| 80 | 80 | or higher on the repository, a member or an | |
| 81 | − | [outside collaborator](/guides/access-and-roles/#outside-collaborators); | |
| 82 | − | - pull requests from g1t's agents. | |
| 81 | + | [outside collaborator](/guides/access-and-roles/#outside-collaborators). | |
| 82 | + | For one g1t made, that is whoever asked for it; | |
| 83 | + | - pull requests g1t opened on its own, such as a security update. | |
| 83 | 84 | ||
| 84 | 85 | Anyone else's pull request, such as one from a fork or by someone with | |
| 85 | 86 | Read or Triage (who may open one on a private repository too), runs its |
| 75 | 75 | | `repo.collaborator_added`, `repo.collaborator_role_changed`, `repo.collaborator_removed` | Someone was given a role on it, had their role changed, or lost it. `data.username`, `data.role`, `data.previous_role`. See [access and roles](/guides/access-and-roles/). | | |
| 76 | 76 | | `repo.archived`, `repo.unarchived` | It was made read-only, or writable again. | | |
| 77 | 77 | | `repo.deleted`, `repo.restored`, `repo.purged` | It was deleted, restored within its 30 days, or removed for good. | | |
| 78 | − | | `issue.opened`, `issue.updated`, `issue.assigned`, `issue.closed`, `issue.reopened` | An issue changed. `data.number`; on close, `data.reason` and `data.resolved_by`. | | |
| 78 | + | | `issue.opened`, `issue.updated`, `issue.assigned`, `issue.closed`, `issue.reopened` | An issue changed. `data.number` and `data.author` (`id` and `username`); on close, `data.reason` and `data.resolved_by`. For an issue g1t's agent filed while at work, `data.author` is g1t and `data.requested_by` is the person it was working for. | | |
| 79 | 79 | | `comment.created` | A comment or review on an issue or pull request. | | |
| 80 | − | | `pull.opened`, `pull.ready`, `pull.updated`, `pull.merge_requested`, `pull.merged`, `pull.closed` | A pull request changed. `data.number`, `data.issue`; on merge, `data.commit`. On a change by g1t, once g1t has worked it out, `data.confidence`: `level` (`high`, `medium` or `low`), `reasons`, `self_reported`, `uncertain_about`, `run_id` and `assessed_at`. See [how sure the agent is](/guides/working-with-g1t/#how-sure-the-agent-is). | | |
| 80 | + | | `pull.opened`, `pull.ready`, `pull.updated`, `pull.merge_requested`, `pull.merged`, `pull.closed` | A pull request changed. `data.number`, `data.issue` and `data.author` (`id` and `username`); on merge, `data.commit`. For a change g1t made, `data.author` is g1t and `data.requested_by` is the person who asked for it; `actor` is still whoever caused the event. On a change by g1t, once g1t has worked it out, `data.confidence`: `level` (`high`, `medium` or `low`), `reasons`, `self_reported`, `uncertain_about`, `run_id` and `assessed_at`. See [how sure the agent is](/guides/working-with-g1t/#how-sure-the-agent-is). | | |
| 81 | 81 | | `checks.completed` | A pull request's checks finished: every status on its head has reported and none is still pending, or the merge queue took it out. `data.number`, `data.commit`, and `data.status`, `passed` or `failed`. | | |
| 82 | 82 | | `review.completed` | g1t reviewed a pull request. `data.verdict`. | | |
| 83 | 83 | | `workflow.completed` | A [workflow](/guides/actions/) run finished. `data.workflow`, `data.conclusion`, `data.run_id`, `data.sha`, `data.pull`. | |
| 13 | 13 | commits, comments, reviews, plans and security updates, assignments, | |
| 14 | 14 | timeline events, the audit log, notifications and webhooks. A pull request | |
| 15 | 15 | g1t opens shows g1t as its author, with **requested by** naming the person | |
| 16 | − | who asked for it. | |
| 16 | + | who asked for it. See [who a pull request is for](#who-a-pull-request-is-for). | |
| 17 | 17 | ||
| 18 | 18 | g1t's runs are paid for by the workspace they work for, so they need a | |
| 19 | 19 | paid workspace or the free trial; see [who can run agents](#who-can-run-agents) | |
| 479 | 479 | A pull request g1t opens has `g1t` as its author and as its `agent` in the | |
| 480 | 480 | API, and its commits are authored `g1t <g1t@users.noreply.g1t.sh>`. | |
| 481 | 481 | ||
| 482 | + | ## Who a pull request is for | |
| 483 | + | ||
| 484 | + | g1t is the author of every pull request it makes and of every issue it | |
| 485 | + | files while at work. The person who asked for the work, by assigning the | |
| 486 | + | issue or handing g1t the task, is kept beside it as **requested by**. Work | |
| 487 | + | g1t starts itself, such as a [security update](/guides/security/), names | |
| 488 | + | nobody. | |
| 489 | + | ||
| 490 | + | | Where | Author | Who asked | | |
| 491 | + | | --- | --- | --- | | |
| 492 | + | | The pull request's page, lists and link previews | **g1t** | **requested by** *name*, or **for** *name* | | |
| 493 | + | | The API and MCP (`get_pull_request`, `list_pull_requests`, `get_issue`) | `author`: `{ "username": "g1t", "kind": "agent" }` | `requested_by`, or `null` | | |
| 494 | + | | [Webhooks](/guides/webhooks/) | `data.author` | `data.requested_by`, left out when nobody asked | | |
| 495 | + | | [Actions](/guides/actions/) (`github.event`) | `pull_request.user`, a `Bot` named `g1t` | `pull_request.requested_by`; `sender` is whoever caused the event | | |
| 496 | + | | [Search](/guides/search/) | `author:g1t` finds it | shown as **for** *name* | | |
| 497 | + | ||
| 498 | + | The person who asked answers for the pull request as its author would: | |
| 499 | + | ||
| 500 | + | - they can update, close and mark it ready, catch it up and steer its | |
| 501 | + | agent without the Triage role; | |
| 502 | + | - they are never asked to review it, and they cannot approve it or | |
| 503 | + | request changes on it; nor does their approval count toward the | |
| 504 | + | repository's required approvals; | |
| 505 | + | - it is on their own lists: what they are working on, and their profile; | |
| 506 | + | - the sandboxes that work on it act as them, so it reaches what they can; | |
| 507 | + | - its workflows and preview get secrets only when they have the Write | |
| 508 | + | role or higher, as theirs would. | |
| 509 | + | ||
| 510 | + | Being its author gives g1t nothing more: a review by g1t's agent still | |
| 511 | + | counts where the repository lets an agent's approval count. | |
| 512 | + | ||
| 482 | 513 | ## How model traffic is routed | |
| 483 | 514 | ||
| 484 | 515 | g1t's runs send model requests to g1t's model proxy at |
| 373 | 373 | ||
| 374 | 374 | - **Overview:** pull requests merged, open pull requests and issues | |
| 375 | 375 | opened, and your most recent activity. | |
| 376 | − | - **Pull requests** and **Issues:** everything you opened, newest first, | |
| 376 | + | - **Pull requests** and **Issues:** everything you opened, and what g1t | |
| 377 | + | opened for you, newest first, | |
| 377 | 378 | with filters beside the list for state (open, closed, merged), type, | |
| 378 | 379 | repository and sort order. Add `?tab=pulls&state=merged` and the like to | |
| 379 | 380 | link to a filtered list. |
| 301 | 301 | | [`record_session`](/reference/api/sessions/record-session/) | Append entries to a pull request's session. Each has `kind` and `text`, and `tool` for tool entries. | `repo`, `number`, `entries` | `pull_requests:write` | | |
| 302 | 302 | | [`read_session`](/reference/api/sessions/read-session/) | The recorded session, oldest first. `after` skips to entries after a sequence number. | `repo`, `number` | `pull_requests:read` | | |
| 303 | 303 | | [`ready`](/reference/api/pull-requests/mark-pull-request-ready/) | Mark a draft ready for review. The summary becomes its description. | `repo`, `number`, `summary` | `pull_requests:write` | | |
| 304 | − | | [`review`](/reference/api/pull-requests/review-pull-request/) | `approve`, or `request_changes` with a `body`. Not on your own pull request. | `repo`, `number`, `verdict` | `pull_requests:write` | | |
| 304 | + | | [`review`](/reference/api/pull-requests/review-pull-request/) | `approve`, or `request_changes` with a `body`. Not on your own pull request, nor one g1t made for you. | `repo`, `number`, `verdict` | `pull_requests:write` | | |
| 305 | 305 | | [`close`](/reference/api/pull-requests/close-pull-request/) | Close it without merging. | `repo`, `number` | `pull_requests:write` | | |
| 306 | 306 | | [`merge`](/reference/api/pull-requests/merge-pull-request/) | Land it on `main` and resolve its issue, or add it to the [merge queue](/guides/merge-queue/), once every [required check](/guides/pull-requests/#required-status-checks) has passed on its head. `ignore_checks` bypasses them where the repository allows it. Write role. | `repo`, `number` | `pull_requests:write` | | |
| 307 | 307 | | [`merge_queue`](/reference/api/pull-requests/get-merge-queue/) | The pull requests waiting to land, in order, each with the state it is tested in and how that went; then those that recently landed or left. | `repo` | `pull_requests:read` | |
Binary or large file; its contents are not shown.
| 257 | 257 | <Sparkles size={11} /> | |
| 258 | 258 | written by {current.pull.agent} | |
| 259 | 259 | </span> | |
| 260 | − | {current.pull.agent === "g1t" && <span>for {current.pull.author}</span>} | |
| 260 | + | {current.pull.requestedBy && <span>for {current.pull.requestedBy}</span>} | |
| 261 | 261 | </> | |
| 262 | 262 | ) : ( | |
| 263 | 263 | <span>by {current.pull.author} with {current.pull.agent}</span> |
| 6 | 6 | ||
| 7 | 7 | import { Avatar, TimeAgo } from "./ui"; | |
| 8 | 8 | import { Badge } from "./ui/badge"; | |
| 9 | + | import { PersonLink } from "./work"; | |
| 9 | 10 | ||
| 10 | 11 | /** Text with the parts that matched the query marked. */ | |
| 11 | 12 | export function Highlighted({ parts, className }: { parts: Segment[]; className?: string }) { | |
| 158 | 159 | {hit.snippet.length > 0 && <Highlighted parts={hit.snippet} className="mt-1 block text-sm text-muted" />} | |
| 159 | 160 | <div className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-faint"> | |
| 160 | 161 | {hit.author && ( | |
| 161 | − | <Link to={`/u/${hit.author}`} className="hover:text-fg"> | |
| 162 | − | {hit.author} | |
| 163 | − | </Link> | |
| 162 | + | <span> | |
| 163 | + | <PersonLink name={hit.author} className="hover:text-fg" /> | |
| 164 | + | {hit.requestedBy && ( | |
| 165 | + | <> | |
| 166 | + | {" "} | |
| 167 | + | for{" "} | |
| 168 | + | <PersonLink name={hit.requestedBy} className="hover:text-fg" /> | |
| 169 | + | </> | |
| 170 | + | )} | |
| 171 | + | </span> | |
| 164 | 172 | )} | |
| 165 | 173 | {hit.labels.map((label) => ( | |
| 166 | 174 | <span key={label} className="rounded-full px-2 py-px text-muted ring-1 ring-line"> |
| 1 | 1 | import assert from "node:assert/strict"; | |
| 2 | 2 | import { test } from "node:test"; | |
| 3 | 3 | ||
| 4 | + | import { workOwner } from "../../../../packages/contracts/src/work.ts"; | |
| 4 | 5 | import { madeByG1t, openedBy } from "./opened-by.ts"; | |
| 5 | 6 | ||
| 6 | − | const author = { id: "usr_1", username: "syntaqx" }; | |
| 7 | + | const syntaqx = { id: "usr_1", username: "syntaqx" }; | |
| 8 | + | const g1t = { id: "usr_g1t_agent", username: "g1t", kind: "agent" as const }; | |
| 7 | 9 | ||
| 8 | − | test("a pull request g1t's agent made in a fork shows g1t, requested by its author", () => { | |
| 9 | − | const pull = { agent: "g1t", branch: null, author }; | |
| 10 | + | test("a pull request g1t made shows g1t, requested by whoever asked", () => { | |
| 11 | + | const pull = { agent: "g1t", branch: null, author: g1t, requestedBy: syntaqx }; | |
| 10 | 12 | assert.equal(madeByG1t(pull), true); | |
| 11 | 13 | assert.deepEqual(openedBy(pull), { name: "g1t", requestedBy: "syntaqx" }); | |
| 14 | + | // It is theirs to manage, as their own would be. | |
| 15 | + | assert.equal(workOwner(pull).id, "usr_1"); | |
| 12 | 16 | }); | |
| 13 | 17 | ||
| 14 | 18 | test("anything else shows its author", () => { | |
| 15 | 19 | // Someone's own agent. | |
| 16 | − | assert.deepEqual(openedBy({ agent: "claude-code", branch: null, author }), { name: "syntaqx", requestedBy: null }); | |
| 20 | + | assert.deepEqual(openedBy({ author: syntaqx, requestedBy: null }), { name: "syntaqx", requestedBy: null }); | |
| 17 | 21 | // A branch pushed to the repository, even with g1t named on it. | |
| 18 | 22 | assert.equal(madeByG1t({ agent: "g1t", branch: "fix-login" }), false); | |
| 19 | − | assert.deepEqual(openedBy({ agent: "g1t", branch: "fix-login", author }), { name: "syntaqx", requestedBy: null }); | |
| 23 | + | assert.equal(workOwner({ author: syntaqx, requestedBy: null }).id, "usr_1"); | |
| 20 | 24 | }); | |
| 21 | 25 | ||
| 22 | 26 | test("work g1t started itself names nobody as asking", () => { | |
| 23 | − | const pull = { agent: "g1t", branch: null, author: { id: "g1t", username: "g1t" } }; | |
| 27 | + | const pull = { author: { id: "g1t", username: "g1t", kind: "system" as const }, requestedBy: null }; | |
| 24 | 28 | assert.deepEqual(openedBy(pull), { name: "g1t", requestedBy: null }); | |
| 29 | + | assert.equal(workOwner(pull).username, "g1t"); | |
| 25 | 30 | }); |
| 12 | 12 | } | |
| 13 | 13 | ||
| 14 | 14 | /** | |
| 15 | − | * Who shows as having opened a pull request. g1t for one its agent made, | |
| 16 | − | * with the person who asked for the work as `requestedBy`; anyone else's | |
| 17 | − | * is its author's, with no `requestedBy`. Only the face: who may change | |
| 18 | − | * the pull request is still decided by its stored author. | |
| 15 | + | * Who opened an issue or a pull request, and who asked g1t for it, as | |
| 16 | + | * stored: g1t for what it made or filed, with the person it was for as | |
| 17 | + | * `requestedBy`; anyone else's, theirs alone. Who may change it is the | |
| 18 | + | * same pair read the other way round: `workOwner` in the contracts. | |
| 19 | 19 | */ | |
| 20 | − | export function openedBy(pull: Pick<Pull, "agent" | "branch" | "author">): { name: string; requestedBy: string | null } { | |
| 21 | − | if (!madeByG1t(pull)) return { name: pull.author.username, requestedBy: null }; | |
| 22 | − | // Work g1t started itself, such as a security update's code change, was asked for by nobody else. | |
| 23 | − | return { name: G1T, requestedBy: pull.author.username === G1T ? null : pull.author.username }; | |
| 20 | + | export function openedBy(item: Pick<Pull, "author" | "requestedBy">): { name: string; requestedBy: string | null } { | |
| 21 | + | return { name: item.author.username, requestedBy: item.requestedBy?.username ?? null }; | |
| 24 | 22 | } |
| 10 | 10 | type RepoPath, | |
| 11 | 11 | REPO_ROLE_LABELS, | |
| 12 | 12 | isActiveRun, | |
| 13 | + | workOwner, | |
| 13 | 14 | } from "@g1t/contracts"; | |
| 14 | 15 | ||
| 15 | 16 | import type { Route } from "./+types/home"; | |
| 240 | 241 | ||
| 241 | 242 | const lower = username.toLowerCase(); | |
| 242 | 243 | const openPulls = (perRepo ?? []).flatMap(({ repo, pulls }) => (pulls ?? []).map((pull) => ({ pull, repo }))); | |
| 244 | + | // Never your own, nor one g1t made for you. | |
| 243 | 245 | const reviewRequested = openPulls.filter( | |
| 244 | 246 | ({ pull }) => | |
| 245 | 247 | pull.status === "open" && | |
| 246 | − | pull.author.username.toLowerCase() !== lower && | |
| 248 | + | workOwner(pull).username.toLowerCase() !== lower && | |
| 247 | 249 | pull.reviewers.some((name) => name.toLowerCase() === lower), | |
| 248 | 250 | ); | |
| 249 | 251 | ||
| 258 | 260 | repo: pathOf(repo), | |
| 259 | 261 | ref: `#${pull.number}`, | |
| 260 | 262 | by: agentWork ? who(pull.agent) : who(pull.author.username), | |
| 261 | − | for: agentWork ? pull.author.username : null, | |
| 263 | + | for: agentWork ? workOwner(pull).username : null, | |
| 262 | 264 | facts: pullFacts({ | |
| 263 | 265 | checkStatus: pull.checkStatus, | |
| 264 | 266 | files: pull.files, | |
| 398 | 400 | key, | |
| 399 | 401 | kind: "review", | |
| 400 | 402 | title: pull.title, | |
| 401 | − | detail: `${pull.author.username} asked for your review.`, | |
| 403 | + | detail: `${workOwner(pull).username} asked for your review.`, | |
| 402 | 404 | to: `/${repo.namespace}/${repo.name}/pull/${pull.number}`, | |
| 403 | 405 | action: "Review", | |
| 404 | 406 | at: Date.parse(pull.updatedAt), |
| 3 | 3 | import { useEffect } from "react"; | |
| 4 | 4 | import { Form, Link, redirect, useNavigation, useRevalidator } from "react-router"; | |
| 5 | 5 | ||
| 6 | − | import { type Pull, PROVIDERS } from "@g1t/contracts"; | |
| 6 | + | import { type Pull, PROVIDERS, workOwner } from "@g1t/contracts"; | |
| 7 | 7 | ||
| 8 | 8 | import type { Route } from "./+types/issue"; | |
| 9 | 9 | import { excerpt, page } from "../../lib/meta"; | |
| 52 | 52 | return page(args, { | |
| 53 | 53 | title: `${title}${params.owner}/${params.repo} · g1t`, | |
| 54 | 54 | description: issue | |
| 55 | − | ? `${state} issue #${issue.number} on ${params.owner}/${params.repo}, opened by ${issue.author.username}.${body ? ` ${body}` : ""}` | |
| 55 | + | ? `${state} issue #${issue.number} on ${params.owner}/${params.repo}, opened by ${issue.author.username}${issue.requestedBy ? ` for ${issue.requestedBy.username}` : ""}.${body ? ` ${body}` : ""}` | |
| 56 | 56 | : null, | |
| 57 | 57 | // The card shows the title and the state. | |
| 58 | 58 | version: issue ? [issue.title, issue.state, issue.reason] : undefined, | |
| 94 | 94 | computeNote, | |
| 95 | 95 | links, | |
| 96 | 96 | members: members?.ok ? members.value.map((member) => member.username) : [], | |
| 97 | − | // The author can close and reopen their own issue; Triage and up, anyone's. | |
| 98 | − | canManage: viewer != null && (viewer.id === issue.author.id || can.triage), | |
| 97 | + | // The author can close and reopen their own issue, and whoever g1t's | |
| 98 | + | // agent filed one for, that one; Triage and up, anyone's. | |
| 99 | + | canManage: viewer != null && (viewer.id === workOwner(issue).id || can.triage), | |
| 99 | 100 | can, | |
| 100 | 101 | }; | |
| 101 | 102 | } | |
| 283 | 284 | <Avatar name={issue.author.username} size={18} /> | |
| 284 | 285 | <span> | |
| 285 | 286 | <PersonLink name={issue.author.username} className="font-medium text-fg hover:underline" /> opened this{" "} | |
| 287 | + | {issue.requestedBy && ( | |
| 288 | + | <> | |
| 289 | + | for{" "} | |
| 290 | + | <PersonLink name={issue.requestedBy.username} className="font-medium text-fg-soft hover:underline" />{" "} | |
| 291 | + | </> | |
| 292 | + | )} | |
| 286 | 293 | <TimeAgo at={issue.createdAt} /> | |
| 287 | 294 | </span> | |
| 288 | 295 | </span> |
| 219 | 219 | <span className="mt-0.5 block text-xs text-faint"> | |
| 220 | 220 | #{issue.number} opened <TimeAgo at={issue.createdAt} /> by{" "} | |
| 221 | 221 | {issue.author.username} | |
| 222 | + | {issue.requestedBy && <> for {issue.requestedBy.username}</>} | |
| 222 | 223 | {issue.resolvedBy != null && ( | |
| 223 | 224 | <span className="text-merged"> · resolved by #{issue.resolvedBy}</span> | |
| 224 | 225 | )} |
| 21 | 21 | import { type ReactNode, Suspense } from "react"; | |
| 22 | 22 | import { Await, Form, Link, useNavigation } from "react-router"; | |
| 23 | 23 | ||
| 24 | − | import { type AgentRun, type G1tEvent, type Memory, type Pull, RUN_KIND_LABEL, isActiveRun } from "@g1t/contracts"; | |
| 24 | + | import { type AgentRun, type G1tEvent, type Memory, type Pull, RUN_KIND_LABEL, isActiveRun, workOwner } from "@g1t/contracts"; | |
| 25 | 25 | ||
| 26 | 26 | import type { Route } from "./+types/overview"; | |
| 27 | 27 | import { host, StatusDot } from "../../components/deploy"; | |
| 252 | 252 | } else if (lifecycle?.stage === "ready") { | |
| 253 | 253 | needs.push({ key: `pull:${pull.number}`, kind: "ready", title: pull.title, detail: "Ready to land when you merge it.", to, action: "Merge", at: Date.parse(pull.updatedAt), where }); | |
| 254 | 254 | } | |
| 255 | − | if (me && pull.status === "open" && pull.author.username.toLowerCase() !== me && pull.reviewers.some((name) => name.toLowerCase() === me)) { | |
| 256 | − | needs.push({ key: `review:${pull.number}`, kind: "review", title: pull.title, detail: `${pull.author.username} asked for your review.`, to, action: "Review", at: Date.parse(pull.updatedAt), where }); | |
| 255 | + | // Never your own, nor one g1t made for you; whoever it is for asked. | |
| 256 | + | const owner = workOwner(pull).username; | |
| 257 | + | if (me && pull.status === "open" && owner.toLowerCase() !== me && pull.reviewers.some((name) => name.toLowerCase() === me)) { | |
| 258 | + | needs.push({ key: `review:${pull.number}`, kind: "review", title: pull.title, detail: `${owner} asked for your review.`, to, action: "Review", at: Date.parse(pull.updatedAt), where }); | |
| 257 | 259 | } | |
| 258 | 260 | if (member && pull.status === "open" && pull.checkStatus === "failed" && !runOn.has(pull.number) && !lifecycle) { | |
| 259 | 261 | needs.push({ key: `checks:${pull.number}`, kind: "checks", title: pull.title, detail: "It failed in the merge queue and no agent is fixing it.", to, action: "See checks", at: Date.parse(pull.updatedAt), where }); | |
| 836 | 838 | <span>·</span> | |
| 837 | 839 | <span className="inline-flex items-center gap-1"> | |
| 838 | 840 | <Avatar name={pull.agent} size={13} /> | |
| 839 | − | {madeByG1t(pull) | |
| 840 | − | ? `made by g1t for ${pull.author.username}` | |
| 841 | + | {pull.requestedBy | |
| 842 | + | ? `made by ${pull.author.username} for ${pull.requestedBy.username}` | |
| 841 | 843 | : byAgent | |
| 842 | 844 | ? `made by ${pull.agent}` | |
| 843 | 845 | : `by ${pull.author.username}`} |
| 35 | 35 | type SessionEntry, | |
| 36 | 36 | type Viewer, | |
| 37 | 37 | pullComparison, | |
| 38 | + | workOwner, | |
| 38 | 39 | } from "@g1t/contracts"; | |
| 39 | 40 | ||
| 40 | 41 | import type { Route } from "./+types/pull"; | |
| 183 | 184 | viewer, | |
| 184 | 185 | // Write and up can merge. | |
| 185 | 186 | canMerge: can.merge, | |
| 186 | − | // Triage and up manage anyone's pull request; its author, their own. | |
| 187 | − | canManage: can.triage || (viewer != null && viewer.id === pull.author.id), | |
| 187 | + | // Triage and up manage anyone's pull request; its author, their own, | |
| 188 | + | // and whoever asked g1t for one, that one. | |
| 189 | + | canManage: can.triage || (viewer != null && viewer.id === workOwner(pull).id), | |
| 188 | 190 | // Telling its agent things, and re-running checks, spend compute: Write and up. | |
| 189 | 191 | canRun: can.run, | |
| 190 | 192 | // A catch-up is pushed as the viewer: a fork takes pushes only from | |
| 191 | − | // whoever opened it, a branch from anyone who can push. | |
| 192 | − | canUpdate: pull.fork ? viewer?.id === pull.author.id : can.push, | |
| 193 | + | // whoever it is for (who asked g1t for it, or its author), a branch | |
| 194 | + | // from anyone who can push. | |
| 195 | + | canUpdate: pull.fork ? viewer?.id === workOwner(pull).id : can.push, | |
| 193 | 196 | agentsEnabled, | |
| 194 | 197 | computeNote, | |
| 195 | 198 | members: members?.ok ? members.value.map((person) => person.username) : [], | |
| 571 | 574 | const collisions = overlaps.filter((other) => other.issue == null || other.issue !== pull.issue); | |
| 572 | 575 | const review = { | |
| 573 | 576 | changesUrl: here + "?tab=changes", | |
| 574 | − | // Nobody reviews their own pull request. | |
| 575 | − | canJudge: active && viewer != null && viewer.id !== pull.author.id, | |
| 577 | + | // Nobody reviews their own pull request, nor one g1t made for them. | |
| 578 | + | canJudge: active && viewer != null && viewer.id !== workOwner(pull).id, | |
| 576 | 579 | }; | |
| 577 | 580 | // Everyone whose review was asked for, then anyone who reviewed unasked. | |
| 578 | 581 | const reviewerNames = [ | |
| 1245 | 1248 | )} | |
| 1246 | 1249 | <PeoplePicker | |
| 1247 | 1250 | name="reviewer" | |
| 1248 | − | members={members.filter((name) => name !== pull.author.username)} | |
| 1251 | + | members={members.filter((name) => name !== workOwner(pull).username)} | |
| 1249 | 1252 | chosen={pull.reviewers.filter((name) => name !== "g1t")} | |
| 1250 | 1253 | /> | |
| 1251 | 1254 | <Button variant="quiet" type="submit"> |
| 3 | 3 | ||
| 4 | 4 | import type { Route } from "./+types/pulls"; | |
| 5 | 5 | import { page } from "../../lib/meta"; | |
| 6 | − | import { madeByG1t, openedBy } from "../../lib/opened-by"; | |
| 6 | + | import { openedBy } from "../../lib/opened-by"; | |
| 7 | 7 | import { ButtonLink, EmptyState, TimeAgo } from "../../components/ui"; | |
| 8 | 8 | import { CheckBadge } from "../../components/checks"; | |
| 9 | 9 | import { ChangeSize, PullIcon, StateTabs } from "../../components/work"; | |
| 76 | 76 | <span className="mt-0.5 block text-xs text-faint"> | |
| 77 | 77 | #{pull.number} opened <TimeAgo at={pull.createdAt} /> by{" "} | |
| 78 | 78 | {openedBy(pull).name} | |
| 79 | − | {madeByG1t(pull) && <> for {pull.author.username}</>} | |
| 79 | + | {pull.requestedBy && <> for {pull.requestedBy.username}</>} | |
| 80 | 80 | {pull.issue != null && <> · for #{pull.issue}</>} | |
| 81 | 81 | {pull.supersededBy != null && <> · superseded by #{pull.supersededBy}</>} | |
| 82 | 82 | </span> |
| 81 | 81 | agent: pull.agent, | |
| 82 | 82 | runtime: pull.runtime, | |
| 83 | 83 | author: pull.author.username, | |
| 84 | + | requestedBy: pull.requestedBy?.username ?? null, | |
| 84 | 85 | mergedBy: pull.mergedBy, | |
| 85 | 86 | mergedAt: pull.mergedAt, | |
| 86 | 87 | }, |
| 229 | 229 | large tier: planning, catching up and reviews of small changes that | |
| 230 | 230 | touch no sensitive path run small; making changes, other reviews, and | |
| 231 | 231 | any retry after a failed attempt run large. | |
| 232 | + | - **Who a g1t pull request is for:** g1t is the `author` (`username` | |
| 233 | + | `g1t`, `kind` `agent`) of every pull request it makes and every issue it | |
| 234 | + | files at work; `requested_by` is the person who asked (null when nobody | |
| 235 | + | did, as for a security update). That person answers for it as an author | |
| 236 | + | would: they may update, close and steer it without Triage, cannot | |
| 237 | + | approve it, are never asked to review it, see it in their own lists, and | |
| 238 | + | its sandboxes, workflows and previews are trusted as they are. Webhooks | |
| 239 | + | carry `data.author` and `data.requested_by`; Actions payloads | |
| 240 | + | `pull_request.user` (a `Bot`) and `pull_request.requested_by`. | |
| 232 | 241 | - **Put an agent on something in one step:** `POST {repo}/issues/delegate` | |
| 233 | 242 | with `title` and `body` (what to do, in plain words, and what done | |
| 234 | 243 | means if you know it). It opens the issue and assigns g1t at once; it needs the | |
| 398 | 407 | `secrets.G1T_TOKEN` (the workspace's token; `GITHUB_TOKEN` is its alias), | |
| 399 | 408 | which cannot change secrets. A pull request's runs and preview are trusted | |
| 400 | 409 | only when its author has `write` or higher on the repository (a member or | |
| 401 | − | an outside collaborator) or is g1t's agent; anyone else's run with config | |
| 402 | − | only. Guide: | |
| 410 | + | an outside collaborator), or is g1t working on its own; for one g1t made, | |
| 411 | + | the role of whoever asked for it (`requested_by`) counts. Anyone else's run | |
| 412 | + | with config only. Guide: | |
| 403 | 413 | https://docs.g1t.sh/guides/secrets-and-variables/ | |
| 404 | 414 | ||
| 405 | 415 | Self-hosted runners: a job with `runs-on: self-hosted` (or |
| 150 | 150 | pub username: String, | |
| 151 | 151 | } | |
| 152 | 152 | ||
| 153 | + | impl From<&User> for Principal { | |
| 154 | + | fn from(user: &User) -> Self { | |
| 155 | + | Principal { | |
| 156 | + | id: user.id.clone(), | |
| 157 | + | username: user.username.clone(), | |
| 158 | + | } | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | ||
| 153 | 162 | /// Set on a [`User`] resolved from an agent's token: the composite | |
| 154 | 163 | /// identity, "g1t on behalf of syntaqx", and what it may do. | |
| 155 | 164 | #[derive(Clone, Debug, Serialize, Deserialize)] |
| 79 | 79 | pub issue_id: String, | |
| 80 | 80 | pub repo_id: String, | |
| 81 | 81 | pub number: u32, | |
| 82 | + | /// Who opened it: g1t, for one its agent filed while at work. | |
| 83 | + | #[serde(skip_serializing_if = "Option::is_none")] | |
| 84 | + | pub author: Option<crate::credentials::Principal>, | |
| 85 | + | /// For an issue g1t's agent filed: the person it was working for. | |
| 86 | + | #[serde(skip_serializing_if = "Option::is_none")] | |
| 87 | + | pub requested_by: Option<crate::credentials::Principal>, | |
| 82 | 88 | #[serde(skip_serializing_if = "Option::is_none")] | |
| 83 | 89 | pub title: Option<String>, | |
| 84 | 90 | /// On close: `completed` or `not_planned`. | |
| 100 | 106 | pub pull_id: String, | |
| 101 | 107 | pub repo_id: String, | |
| 102 | 108 | pub number: u32, | |
| 109 | + | /// Who opened it: g1t, for a change g1t made. | |
| 110 | + | #[serde(skip_serializing_if = "Option::is_none")] | |
| 111 | + | pub author: Option<crate::credentials::Principal>, | |
| 112 | + | /// For a change g1t made: the person who asked for it. | |
| 113 | + | #[serde(skip_serializing_if = "Option::is_none")] | |
| 114 | + | pub requested_by: Option<crate::credentials::Principal>, | |
| 103 | 115 | /// The number of the issue it is for. | |
| 104 | 116 | #[serde(skip_serializing_if = "Option::is_none")] | |
| 105 | 117 | pub issue: Option<u32>, |
| 155 | 155 | /// `closed` for a pull request. | |
| 156 | 156 | #[serde(default)] | |
| 157 | 157 | pub state: Option<String>, | |
| 158 | − | /// Who opened the issue or pull request. | |
| 158 | + | /// Who opened the issue or pull request: `g1t` for one g1t made. | |
| 159 | 159 | #[serde(default)] | |
| 160 | 160 | pub author: Option<String>, | |
| 161 | + | /// For an issue or pull request g1t opened: who asked for it. | |
| 162 | + | #[serde(default)] | |
| 163 | + | pub requested_by: Option<String>, | |
| 161 | 164 | #[serde(default)] | |
| 162 | 165 | pub labels: Vec<String>, | |
| 163 | 166 | /// A repository's topics. | |
| 191 | 194 | number: None, | |
| 192 | 195 | state: None, | |
| 193 | 196 | author: None, | |
| 197 | + | requested_by: None, | |
| 194 | 198 | labels: Vec::new(), | |
| 195 | 199 | topics: Vec::new(), | |
| 196 | 200 | slug: None, |
| 62 | 62 | pub reason: Option<IssueReason>, | |
| 63 | 63 | /// The number of the pull request whose merge closed this issue. | |
| 64 | 64 | pub resolved_by: Option<u32>, | |
| 65 | + | /// Who opened it: a person, an integration, or g1t (`kind` `agent`) | |
| 66 | + | /// for one its agent filed while at work. | |
| 65 | 67 | pub author: User, | |
| 68 | + | /// For an issue g1t's agent filed: the person it was working for. They | |
| 69 | + | /// may manage it as its author could. See [`owner`](Issue::owner). | |
| 70 | + | #[serde(default)] | |
| 71 | + | pub requested_by: Option<User>, | |
| 66 | 72 | /// RFC 3339. | |
| 67 | 73 | pub created_at: String, | |
| 68 | 74 | /// RFC 3339. | |
| 176 | 182 | /// g1t agent was asked. | |
| 177 | 183 | #[serde(default)] | |
| 178 | 184 | pub reviewers: Vec<String>, | |
| 185 | + | /// Who opened it: a person, or g1t (`kind` `agent`, username `g1t`) | |
| 186 | + | /// for a change g1t made. | |
| 179 | 187 | pub author: User, | |
| 188 | + | /// For a change g1t made: the person who asked for it, by assigning | |
| 189 | + | /// an issue or handing g1t the work. They answer for it as its author | |
| 190 | + | /// would. See [`owner`](Pull::owner). | |
| 191 | + | #[serde(default)] | |
| 192 | + | pub requested_by: Option<User>, | |
| 180 | 193 | /// RFC 3339. | |
| 181 | 194 | pub created_at: String, | |
| 182 | 195 | /// RFC 3339. | |
| 263 | 276 | pub paths: Vec<String>, | |
| 264 | 277 | } | |
| 265 | 278 | ||
| 279 | + | impl Issue { | |
| 280 | + | /// Who the issue is theirs to manage: the person g1t's agent filed it | |
| 281 | + | /// for, or its author. They may edit, close and reopen it without the | |
| 282 | + | /// Triage role. | |
| 283 | + | pub fn owner(&self) -> &User { | |
| 284 | + | self.requested_by.as_ref().unwrap_or(&self.author) | |
| 285 | + | } | |
| 286 | + | } | |
| 287 | + | ||
| 266 | 288 | impl Pull { | |
| 289 | + | /// Who the pull request is theirs to answer for: whoever asked g1t to | |
| 290 | + | /// make it, or its author. Every rule that once read "its author" reads | |
| 291 | + | /// this: they may update, close and steer it, are never asked to review | |
| 292 | + | /// it and cannot approve it, see it as theirs, and a sandbox at work on | |
| 293 | + | /// it acts as them. | |
| 294 | + | pub fn owner(&self) -> &User { | |
| 295 | + | self.requested_by.as_ref().unwrap_or(&self.author) | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | /// Whether `id` is its [`owner`](Pull::owner)'s. | |
| 299 | + | pub fn is_owned_by(&self, id: &str) -> bool { | |
| 300 | + | self.owner().id == id | |
| 301 | + | } | |
| 302 | + | ||
| 267 | 303 | /// What to ask the repos service to see what this pull request changes. | |
| 268 | 304 | /// | |
| 269 | 305 | /// A fork is compared as a whole. A branch is compared by name while | |
| 290 | 326 | } | |
| 291 | 327 | } | |
| 292 | 328 | ||
| 329 | + | /// g1t's agent, as the author of what it opens: stored by | |
| 330 | + | /// [`AGENT_ID`](crate::identity::AGENT_ID), shown as `g1t`. | |
| 331 | + | pub fn g1t_author() -> User { | |
| 332 | + | User { | |
| 333 | + | id: crate::identity::AGENT_ID.to_owned(), | |
| 334 | + | username: crate::identity::AGENT_NAME.to_owned(), | |
| 335 | + | kind: crate::PrincipalKind::Agent, | |
| 336 | + | ..User::default() | |
| 337 | + | } | |
| 338 | + | } | |
| 339 | + | ||
| 340 | + | /// Who is recorded as opening an issue or a pull request that `actor` | |
| 341 | + | /// opens, and who asked for it: `(author, requested_by)`. | |
| 342 | + | /// | |
| 343 | + | /// - g1t's agent, acting for someone through its token (an issue it files | |
| 344 | + | /// while at work): g1t, requested by that person. | |
| 345 | + | /// - A person having g1t make the change (`by_g1t`: a hosted g1t agent in | |
| 346 | + | /// a fork of its own): g1t, requested by them. | |
| 347 | + | /// - Anyone else, and g1t's own work that nobody asked for: the actor, and | |
| 348 | + | /// nobody asking. | |
| 349 | + | pub fn authorship(actor: &User, by_g1t: bool) -> (User, Option<User>) { | |
| 350 | + | let person = |id: &str, username: &str| User { | |
| 351 | + | id: id.to_owned(), | |
| 352 | + | username: username.to_owned(), | |
| 353 | + | kind: crate::PrincipalKind::User, | |
| 354 | + | ..User::default() | |
| 355 | + | }; | |
| 356 | + | if actor.kind == crate::PrincipalKind::Agent { | |
| 357 | + | let asked = actor | |
| 358 | + | .acting | |
| 359 | + | .as_ref() | |
| 360 | + | .map(|acting| &acting.on_behalf_of) | |
| 361 | + | .filter(|on_behalf_of| !crate::system::is_system_id(&on_behalf_of.id)) | |
| 362 | + | .map(|on_behalf_of| person(&on_behalf_of.id, &on_behalf_of.username)); | |
| 363 | + | return (g1t_author(), asked); | |
| 364 | + | } | |
| 365 | + | if by_g1t && actor.kind != crate::PrincipalKind::System { | |
| 366 | + | return (g1t_author(), Some(person(&actor.id, &actor.username))); | |
| 367 | + | } | |
| 368 | + | let author = User { | |
| 369 | + | id: actor.id.clone(), | |
| 370 | + | username: actor.username.clone(), | |
| 371 | + | kind: actor.kind, | |
| 372 | + | ..User::default() | |
| 373 | + | }; | |
| 374 | + | (author, None) | |
| 375 | + | } | |
| 376 | + | ||
| 293 | 377 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
| 294 | 378 | #[serde(rename_all = "lowercase")] | |
| 295 | 379 | pub enum CheckStatus { | |
| 727 | 811 | pub branch: String, | |
| 728 | 812 | /// The change's commit. | |
| 729 | 813 | pub head: String, | |
| 730 | − | /// Who opened the pull request, and so can read its source. | |
| 814 | + | /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for | |
| 815 | + | /// it, or its author), and so can read its source. | |
| 731 | 816 | pub author: User, | |
| 732 | 817 | } | |
| 733 | 818 | ||
| 812 | 897 | } | |
| 813 | 898 | ||
| 814 | 899 | /// `message_agent`: sends the agent working on a pull request a message. | |
| 815 | − | /// The pull request's author and members of the workspace may. Returns | |
| 900 | + | /// The pull request's owner ([`Pull::owner`]: whoever asked g1t for it, or | |
| 901 | + | /// its author) and members of the workspace may. Returns | |
| 816 | 902 | /// `Outcome<AgentMessage>`. | |
| 817 | 903 | #[derive(Debug, Serialize, Deserialize)] | |
| 818 | 904 | pub struct MessageAgentArgs { | |
| 919 | 1005 | pub pull_id: String, | |
| 920 | 1006 | pub repo: RepoPath, | |
| 921 | 1007 | pub number: u32, | |
| 922 | − | /// Who the pull request belongs to. Sandboxes act as them. | |
| 1008 | + | /// Who the pull request belongs to ([`Pull::owner`]: whoever asked g1t | |
| 1009 | + | /// for it, or its author). Sandboxes act as them. | |
| 923 | 1010 | pub author: User, | |
| 924 | 1011 | /// The repository holding the change: its fork, or the repository | |
| 925 | 1012 | /// itself for one made on a branch. | |
| 1301 | 1388 | /// Also `catch_up_pull`: brings the pull request up to date with the | |
| 1302 | 1389 | /// default branch without a sandbox where that is safe, as the repos | |
| 1303 | 1390 | /// service's `update_pull_branch` does, after checking that `actor` may | |
| 1304 | − | /// update it: whoever opened it for a fork, any member for a branch. | |
| 1391 | + | /// update it: its owner ([`Pull::owner`]) for a fork, any member for a | |
| 1392 | + | /// branch. | |
| 1305 | 1393 | /// Returns `Outcome<repos::PullBranchUpdate>`; on `needs_agent` nothing was | |
| 1306 | 1394 | /// pushed and the runner's `update` is the way on. | |
| 1307 | 1395 | #[derive(Debug, Serialize, Deserialize)] | |
| 1536 | 1624 | /// The repository holding the commit: the fork, or the repository itself. | |
| 1537 | 1625 | pub source: RepoPath, | |
| 1538 | 1626 | pub commit: String, | |
| 1539 | − | /// Who opened the pull request, and so can read its source. | |
| 1627 | + | /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for | |
| 1628 | + | /// it, or its author), and so can read its source. | |
| 1540 | 1629 | pub author: User, | |
| 1541 | 1630 | /// Username of whoever wrote the checks: the issue's author. | |
| 1542 | 1631 | pub requested_by: String, | |
| 1619 | 1708 | pub description: String, | |
| 1620 | 1709 | /// The issue the pull request is for, which says what it should achieve. | |
| 1621 | 1710 | pub issue: Option<Issue>, | |
| 1622 | − | /// Who opened the pull request, and so can read its source. | |
| 1711 | + | /// Who the pull request is for ([`Pull::owner`]: whoever asked g1t for | |
| 1712 | + | /// it, or its author), and so can read its source. | |
| 1623 | 1713 | pub author: User, | |
| 1624 | 1714 | /// The files it changes, as of its latest push: how large the change | |
| 1625 | 1715 | /// is, which decides the model that reviews it. | |
| 2070 | 2160 | assert_eq!(with_definition_of_done(" Text ", &[]), "Text"); | |
| 2071 | 2161 | } | |
| 2072 | 2162 | } | |
| 2163 | + | ||
| 2164 | + | #[cfg(test)] | |
| 2165 | + | mod authorship_tests { | |
| 2166 | + | use super::*; | |
| 2167 | + | use crate::PrincipalKind; | |
| 2168 | + | use crate::credentials::{Acting, Principal}; | |
| 2169 | + | use crate::identity::{AGENT_ID, AgentScope}; | |
| 2170 | + | ||
| 2171 | + | fn person() -> User { | |
| 2172 | + | User { | |
| 2173 | + | id: "usr_1".into(), | |
| 2174 | + | username: "syntaqx".into(), | |
| 2175 | + | verified: true, | |
| 2176 | + | ..User::default() | |
| 2177 | + | } | |
| 2178 | + | } | |
| 2179 | + | ||
| 2180 | + | fn agent_for(id: &str, username: &str) -> User { | |
| 2181 | + | User { | |
| 2182 | + | id: AGENT_ID.into(), | |
| 2183 | + | username: "g1t".into(), | |
| 2184 | + | kind: PrincipalKind::Agent, | |
| 2185 | + | acting: Some(Box::new(Acting { | |
| 2186 | + | credential_id: "tok_1".into(), | |
| 2187 | + | agent: "g1t".into(), | |
| 2188 | + | on_behalf_of: Principal { id: id.into(), username: username.into() }, | |
| 2189 | + | scope: AgentScope { | |
| 2190 | + | repo: RepoPath { namespace: "acme".into(), name: "web".into() }, | |
| 2191 | + | operations: Vec::new(), | |
| 2192 | + | run: None, | |
| 2193 | + | }, | |
| 2194 | + | })), | |
| 2195 | + | ..User::default() | |
| 2196 | + | } | |
| 2197 | + | } | |
| 2198 | + | ||
| 2199 | + | fn pull(author: User, requested_by: Option<User>) -> Pull { | |
| 2200 | + | let mut pull: Pull = serde_json::from_value(serde_json::json!({ | |
| 2201 | + | "id": "pr_1", "repoId": "rep_1", "number": 14, "issue": 12, "title": "Fix it", "body": null, | |
| 2202 | + | "agent": "g1t", "runtime": "hosted", "status": "open", "fork": null, "forkRepoId": null, | |
| 2203 | + | "branch": null, "headCommit": null, "mergeBase": null, "mergedBy": null, "mergedAt": null, | |
| 2204 | + | "supersededBy": null, "checkStatus": null, | |
| 2205 | + | "author": { "id": "x", "username": "x" }, | |
| 2206 | + | "createdAt": "", "updatedAt": "" | |
| 2207 | + | })) | |
| 2208 | + | .unwrap(); | |
| 2209 | + | pull.author = author; | |
| 2210 | + | pull.requested_by = requested_by; | |
| 2211 | + | pull | |
| 2212 | + | } | |
| 2213 | + | ||
| 2214 | + | #[test] | |
| 2215 | + | fn a_change_a_person_has_g1t_make_is_g1t_s_requested_by_them() { | |
| 2216 | + | let (author, asked) = authorship(&person(), true); | |
| 2217 | + | assert_eq!((author.id.as_str(), author.username.as_str(), author.kind), (AGENT_ID, "g1t", PrincipalKind::Agent)); | |
| 2218 | + | let asked = asked.expect("the person asked for it"); | |
| 2219 | + | assert_eq!((asked.id.as_str(), asked.username.as_str(), asked.kind), ("usr_1", "syntaqx", PrincipalKind::User)); | |
| 2220 | + | } | |
| 2221 | + | ||
| 2222 | + | #[test] | |
| 2223 | + | fn what_g1t_s_agent_files_at_work_is_g1t_s_requested_by_whoever_it_works_for() { | |
| 2224 | + | let (author, asked) = authorship(&agent_for("usr_1", "syntaqx"), false); | |
| 2225 | + | assert_eq!(author.id, AGENT_ID); | |
| 2226 | + | assert_eq!(asked.map(|user| user.username), Some("syntaqx".into())); | |
| 2227 | + | } | |
| 2228 | + | ||
| 2229 | + | #[test] | |
| 2230 | + | fn nobody_asked_for_g1t_s_own_work() { | |
| 2231 | + | // A run g1t started itself acts for g1t, not for a person. | |
| 2232 | + | let (author, asked) = authorship(&agent_for(crate::system::ID, "g1t"), false); | |
| 2233 | + | assert_eq!(author.id, AGENT_ID); | |
| 2234 | + | assert!(asked.is_none()); | |
| 2235 | + | // A security update g1t opens is g1t's own, as before. | |
| 2236 | + | let (author, asked) = authorship(&User::system("acme"), true); | |
| 2237 | + | assert_eq!((author.id.as_str(), author.kind), (crate::system::ID, PrincipalKind::System)); | |
| 2238 | + | assert!(asked.is_none()); | |
| 2239 | + | } | |
| 2240 | + | ||
| 2241 | + | #[test] | |
| 2242 | + | fn anyone_else_opens_their_own() { | |
| 2243 | + | let (author, asked) = authorship(&person(), false); | |
| 2244 | + | assert_eq!((author.id.as_str(), author.kind), ("usr_1", PrincipalKind::User)); | |
| 2245 | + | assert!(asked.is_none()); | |
| 2246 | + | // Nothing but who they are is kept. | |
| 2247 | + | assert!(author.workspaces.is_empty() && author.acting.is_none()); | |
| 2248 | + | } | |
| 2249 | + | ||
| 2250 | + | #[test] | |
| 2251 | + | fn the_requester_owns_g1t_s_pull_request_and_an_author_their_own() { | |
| 2252 | + | let made = pull(g1t_author(), Some(person())); | |
| 2253 | + | assert_eq!(made.owner().id, "usr_1"); | |
| 2254 | + | assert!(made.is_owned_by("usr_1")); | |
| 2255 | + | assert!(!made.is_owned_by(AGENT_ID), "g1t's agent does not answer for its own change"); | |
| 2256 | + | let own = pull(person(), None); | |
| 2257 | + | assert_eq!(own.owner().id, "usr_1"); | |
| 2258 | + | assert!(own.is_owned_by("usr_1")); | |
| 2259 | + | } | |
| 2260 | + | ||
| 2261 | + | #[test] | |
| 2262 | + | fn requested_by_is_null_when_nobody_asked_and_read_as_absent_from_older_senders() { | |
| 2263 | + | let own = pull(person(), None); | |
| 2264 | + | let sent = serde_json::to_value(&own).unwrap(); | |
| 2265 | + | assert!(sent["requestedBy"].is_null()); | |
| 2266 | + | let mut older = sent.clone(); | |
| 2267 | + | older.as_object_mut().unwrap().remove("requestedBy"); | |
| 2268 | + | let read: Pull = serde_json::from_value(older).unwrap(); | |
| 2269 | + | assert!(read.requested_by.is_none()); | |
| 2270 | + | } | |
| 2271 | + | } |
| 82 | 82 | * points to now, and `defaultBranch` whether it is the default branch. | |
| 83 | 83 | */ | |
| 84 | 84 | "git.push": { repoId: string; ref: string; after: string; defaultBranch: boolean }; | |
| 85 | − | "issue.opened": { issueId: string; repoId: string; number: number; title: string }; | |
| 85 | + | /** | |
| 86 | + | * `author` is who opened it: g1t, for one its agent filed while at work, | |
| 87 | + | * with `requestedBy` the person it was working for. Every issue and pull | |
| 88 | + | * request event carries both. | |
| 89 | + | */ | |
| 90 | + | "issue.opened": { | |
| 91 | + | issueId: string; | |
| 92 | + | repoId: string; | |
| 93 | + | number: number; | |
| 94 | + | title: string; | |
| 95 | + | author?: { id: string; username: string }; | |
| 96 | + | requestedBy?: { id: string; username: string }; | |
| 97 | + | }; | |
| 86 | 98 | "issue.updated": { issueId: string; repoId: string; number: number }; | |
| 87 | 99 | /** The people an issue is assigned to changed; `assignees` is the new set. */ | |
| 88 | 100 | "issue.assigned": { issueId: string; repoId: string; number: number; assignees: string[] }; | |
| 95 | 107 | resolvedBy?: number; | |
| 96 | 108 | }; | |
| 97 | 109 | "issue.reopened": { issueId: string; repoId: string; number: number }; | |
| 98 | − | /** `issue` is the number of the issue the pull request is for. */ | |
| 99 | − | "pull.opened": { pullId: string; repoId: string; number: number; issue?: number; agent: string }; | |
| 110 | + | /** | |
| 111 | + | * `issue` is the number of the issue the pull request is for. `author` is | |
| 112 | + | * who opened it: g1t, for a change g1t made, with `requestedBy` the person | |
| 113 | + | * who asked for it. | |
| 114 | + | */ | |
| 115 | + | "pull.opened": { | |
| 116 | + | pullId: string; | |
| 117 | + | repoId: string; | |
| 118 | + | number: number; | |
| 119 | + | issue?: number; | |
| 120 | + | agent: string; | |
| 121 | + | author?: { id: string; username: string }; | |
| 122 | + | requestedBy?: { id: string; username: string }; | |
| 123 | + | }; | |
| 100 | 124 | /** `confidence`, on a g1t agent's change once g1t has worked it out, is on every pull request event. */ | |
| 101 | 125 | "pull.ready": { pullId: string; repoId: string; number: number; issue?: number; confidence?: Confidence }; | |
| 102 | 126 | /** A push moved the head of a pull request that is ready for review. */ |
| 48 | 48 | number: number | null; | |
| 49 | 49 | /** `open`/`closed` for an issue; `draft`/`open`/`merged`/`closed` for a pull request. */ | |
| 50 | 50 | state: string | null; | |
| 51 | + | /** Who opened the issue or pull request: `g1t` for one g1t made. */ | |
| 51 | 52 | author: string | null; | |
| 53 | + | /** For an issue or pull request g1t opened: who asked for it. */ | |
| 54 | + | requestedBy?: string | null; | |
| 52 | 55 | labels: string[]; | |
| 53 | 56 | topics: string[]; | |
| 54 | 57 | /** A username or a workspace's slug. */ |
| 33 | 33 | reason: IssueReason | null; | |
| 34 | 34 | /** The number of the pull request whose merge closed this issue. */ | |
| 35 | 35 | resolvedBy: number | null; | |
| 36 | + | /** Who opened it: a person, an integration, or g1t (`kind` `agent`) for one its agent filed while at work. */ | |
| 36 | 37 | author: User; | |
| 38 | + | /** | |
| 39 | + | * For an issue g1t's agent filed: the person it was working for. They may | |
| 40 | + | * manage it as its author could. See `workOwner`. | |
| 41 | + | */ | |
| 42 | + | requestedBy: User | null; | |
| 37 | 43 | /** RFC 3339. */ | |
| 38 | 44 | createdAt: string; | |
| 39 | 45 | /** RFC 3339. */ | |
| 59 | 65 | agent: string | null; | |
| 60 | 66 | }; | |
| 61 | 67 | ||
| 68 | + | /** | |
| 69 | + | * Whose an issue or a pull request is to answer for: whoever asked g1t for | |
| 70 | + | * it, or its author. They may change, close and steer it, are never asked to | |
| 71 | + | * review it and cannot approve it, and see it as theirs. Mirrors | |
| 72 | + | * `Pull::owner` in the Rust contracts. | |
| 73 | + | */ | |
| 74 | + | export function workOwner(item: Pick<Pull, "author" | "requestedBy">): User { | |
| 75 | + | return item.requestedBy ?? item.author; | |
| 76 | + | } | |
| 77 | + | ||
| 62 | 78 | /** `draft` is still being worked on; `open` is ready for review. */ | |
| 63 | 79 | export type PullStatus = "draft" | "open" | "merged" | "closed"; | |
| 64 | 80 | ||
| 118 | 134 | * agent was asked. | |
| 119 | 135 | */ | |
| 120 | 136 | reviewers: string[]; | |
| 137 | + | /** Who opened it: a person, or g1t (`kind` `agent`, username `g1t`) for a change g1t made. */ | |
| 121 | 138 | author: User; | |
| 139 | + | /** | |
| 140 | + | * For a change g1t made: the person who asked for it, by assigning an issue | |
| 141 | + | * or handing g1t the work. They answer for it as its author would. See | |
| 142 | + | * `workOwner`. | |
| 143 | + | */ | |
| 144 | + | requestedBy: User | null; | |
| 122 | 145 | /** RFC 3339. */ | |
| 123 | 146 | createdAt: string; | |
| 124 | 147 | /** RFC 3339. */ | |
| 292 | 315 | /** The repository holding the commit: the fork, or the repository itself. */ | |
| 293 | 316 | source: RepoPath; | |
| 294 | 317 | commit: string; | |
| 295 | − | /** Who opened the pull request, and so can read its source. */ | |
| 318 | + | /** Who the pull request is for (`workOwner`: whoever asked g1t for it, or its author), and so can read its source. */ | |
| 296 | 319 | author: User; | |
| 297 | 320 | /** Username of whoever wrote the checks: the issue's author. */ | |
| 298 | 321 | requestedBy: string; | |
| 447 | 470 | branch: string; | |
| 448 | 471 | /** The change's commit. */ | |
| 449 | 472 | head: string; | |
| 450 | − | /** Who opened the pull request, and so can read its source. */ | |
| 473 | + | /** Who the pull request is for (`workOwner`: whoever asked g1t for it, or its author), and so can read its source. */ | |
| 451 | 474 | author: User; | |
| 452 | 475 | }; | |
| 453 | 476 | ||
| 494 | 517 | pullId: string; | |
| 495 | 518 | repo: RepoPath; | |
| 496 | 519 | number: number; | |
| 497 | − | /** Who the pull request belongs to. Sandboxes act as them. */ | |
| 520 | + | /** Who the pull request belongs to (`workOwner`: whoever asked g1t for it, or its author). Sandboxes act as them. */ | |
| 498 | 521 | author: User; | |
| 499 | 522 | /** The repository holding the change: its fork, or the repository itself. */ | |
| 500 | 523 | source: RepoPath; | |
| 658 | 681 | description: string; | |
| 659 | 682 | /** The issue the pull request is for, which says what it should achieve. */ | |
| 660 | 683 | issue: Issue | null; | |
| 661 | − | /** Who opened the pull request, and so can read its source. */ | |
| 684 | + | /** Who the pull request is for (`workOwner`: whoever asked g1t for it, or its author), and so can read its source. */ | |
| 662 | 685 | author: User; | |
| 663 | 686 | /** | |
| 664 | 687 | * The files it changes, as of its latest push: how large the change is, |
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
Binary or large file; its contents are not shown.
This change is too large to show in full.