g1t/services/repos/src/land.rs
Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Issues and pull requests replace intents and attempts | 1 | //! Landing a pull request: moving a repository's branch forward to a commit |
| Pull requests from branches | 2 | //! from a fork, or from another of its own branches. |
| Rust repos service with shipping; pull requests kept in the model | 3 | //! |
| 4 | //! The Artifacts binding cannot write, so this speaks git's smart HTTP | |
| 5 | //! protocol directly. It asks the fork for a pack holding exactly the | |
| 6 | //! objects the target is missing, then pushes that pack to the target | |
| 7 | //! unchanged. No object is parsed or rebuilt along the way. | |
| 8 | ||
| 9 | use g1t_contracts::repos::GitAccess; | |
| 10 | use worker::js_sys::Uint8Array; | |
| 11 | use worker::{Error, Fetch, Headers, Method, Request, RequestInit, Result}; | |
| 12 | ||
| 13 | const ZERO_ID: &str = "0000000000000000000000000000000000000000"; | |
| 14 | const FLUSH: &[u8] = b"0000"; | |
| 15 | /// Side-band channels: pack data, and fatal errors. | |
| 16 | const PACK_BAND: u8 = 1; | |
| 17 | const ERROR_BAND: u8 = 3; | |
| 18 | ||
| 19 | fn pkt_line(payload: &str) -> Vec<u8> { | |
| 20 | format!("{:04x}{payload}", payload.len() + 4).into_bytes() | |
| 21 | } | |
| 22 | ||
| 23 | async fn post(access: &GitAccess, service: &str, body: Vec<u8>) -> Result<Vec<u8>> { | |
| 24 | let headers = Headers::new(); | |
| 25 | headers.set("authorization", &format!("Bearer {}", access.token))?; | |
| 26 | headers.set("content-type", &format!("application/x-{service}-request"))?; | |
| 27 | headers.set("accept", &format!("application/x-{service}-result"))?; | |
| 28 | let mut init = RequestInit::new(); | |
| 29 | init.with_method(Method::Post) | |
| 30 | .with_headers(headers) | |
| 31 | .with_body(Some(Uint8Array::from(body.as_slice()).into())); | |
| 32 | let request = Request::new_with_init(&format!("{}/{service}", access.remote), &init)?; | |
| 33 | let mut response = Fetch::Request(request).send().await?; | |
| 34 | let bytes = response.bytes().await?; | |
| 35 | if response.status_code() != 200 { | |
| 36 | return Err(Error::RustError(format!( | |
| 37 | "{service} returned {}: {}", | |
| 38 | response.status_code(), | |
| 39 | String::from_utf8_lossy(&bytes) | |
| 40 | ))); | |
| 41 | } | |
| 42 | Ok(bytes) | |
| 43 | } | |
| 44 | ||
| 45 | /// The payloads of the pkt-lines in `bytes`, and the offset where they stop. | |
| Pull requests from branches | 46 | pub(crate) fn read_pkt_lines(bytes: &[u8]) -> (Vec<&[u8]>, usize) { |
| Rust repos service with shipping; pull requests kept in the model | 47 | let mut lines = Vec::new(); |
| 48 | let mut position = 0; | |
| 49 | while position + 4 <= bytes.len() { | |
| 50 | let Some(length) = std::str::from_utf8(&bytes[position..position + 4]) | |
| 51 | .ok() | |
| 52 | .and_then(|hex| usize::from_str_radix(hex, 16).ok()) | |
| 53 | else { | |
| 54 | break; | |
| 55 | }; | |
| 56 | if length < 4 { | |
| 57 | // Flush, delimiter and response-end packets carry no payload. | |
| 58 | position += 4; | |
| 59 | continue; | |
| 60 | } | |
| 61 | let end = (position + length).min(bytes.len()); | |
| 62 | lines.push(&bytes[position + 4..end]); | |
| 63 | position = end; | |
| 64 | } | |
| 65 | (lines, position) | |
| 66 | } | |
| 67 | ||
| 68 | /// A pack holding everything reachable from `want` that is not reachable | |
| 69 | /// from `have`. | |
| 70 | async fn fetch_pack(source: &GitAccess, want: &str, have: Option<&str>) -> Result<Vec<u8>> { | |
| 71 | // Side-band framing puts the pack in its own channel, so its exact bytes | |
| 72 | // can be recovered. Without it the response ends in a stray flush packet | |
| 73 | // that a receiver rejects as junk after the pack. | |
| 74 | let mut body = pkt_line(&format!("want {want} side-band-64k\n")); | |
| 75 | body.extend_from_slice(FLUSH); | |
| 76 | if let Some(have) = have { | |
| 77 | body.extend(pkt_line(&format!("have {have}\n"))); | |
| 78 | } | |
| 79 | body.extend(pkt_line("done\n")); | |
| 80 | ||
| 81 | let response = post(source, "git-upload-pack", body).await?; | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 82 | unpack_sideband(&response) |
| 83 | } | |
| 84 | ||
| 85 | /// The pack in an upload-pack response that used side-band framing. | |
| 86 | pub(crate) fn unpack_sideband(response: &[u8]) -> Result<Vec<u8>> { | |
| 87 | let (lines, _) = read_pkt_lines(response); | |
| Rust repos service with shipping; pull requests kept in the model | 88 | let mut pack = Vec::new(); |
| 89 | for line in lines { | |
| 90 | match line.first() { | |
| 91 | Some(&PACK_BAND) => pack.extend_from_slice(&line[1..]), | |
| 92 | Some(&ERROR_BAND) => { | |
| 93 | return Err(Error::RustError(format!( | |
| Pull requests from branches | 94 | "the source refused the fetch: {}", |
| Rust repos service with shipping; pull requests kept in the model | 95 | String::from_utf8_lossy(&line[1..]) |
| 96 | ))); | |
| 97 | } | |
| 98 | // ACK and NAK lines, and progress messages. | |
| 99 | _ => {} | |
| 100 | } | |
| 101 | } | |
| 102 | if !pack.starts_with(b"PACK") { | |
| 103 | return Err(Error::RustError(format!( | |
| Pull requests from branches | 104 | "the source did not send a pack: {}", |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 105 | String::from_utf8_lossy(response) |
| Rust repos service with shipping; pull requests kept in the model | 106 | ))); |
| 107 | } | |
| 108 | Ok(pack) | |
| 109 | } | |
| 110 | ||
| 111 | /// Updates `branch` on the target from `old` to `new`, sending `pack`. | |
| 112 | /// `Err(reason)` in the inner result means git refused the update, for | |
| 113 | /// example because the branch is no longer at `old`. | |
| Agents as a team: lifecycle, merge queue, billing and a new shell | 114 | pub(crate) async fn push_pack( |
| Rust repos service with shipping; pull requests kept in the model | 115 | target: &GitAccess, |
| 116 | branch: &str, | |
| 117 | old: Option<&str>, | |
| 118 | new: &str, | |
| 119 | pack: Vec<u8>, | |
| 120 | ) -> Result<std::result::Result<(), String>> { | |
| 121 | let reference = format!("refs/heads/{branch}"); | |
| 122 | let mut body = pkt_line(&format!( | |
| 123 | "{} {new} {reference}\0 report-status\n", | |
| 124 | old.unwrap_or(ZERO_ID) | |
| 125 | )); | |
| 126 | body.extend_from_slice(FLUSH); | |
| 127 | body.extend(pack); | |
| 128 | ||
| 129 | let response = post(target, "git-receive-pack", body).await?; | |
| 130 | let (lines, _) = read_pkt_lines(&response); | |
| 131 | let lines: Vec<String> = lines | |
| 132 | .into_iter() | |
| 133 | .map(|line| String::from_utf8_lossy(line).trim_end().to_owned()) | |
| 134 | .collect(); | |
| 135 | let unpacked = lines.iter().any(|line| line == "unpack ok"); | |
| 136 | let updated = lines.iter().any(|line| *line == format!("ok {reference}")); | |
| 137 | Ok(if unpacked && updated { | |
| 138 | Ok(()) | |
| 139 | } else { | |
| 140 | Err(lines.join("; ")) | |
| 141 | }) | |
| 142 | } | |
| 143 | ||
| 144 | /// Moves `branch` on `target` from `old` to `new`, a commit that exists in | |
| 145 | /// `source`. The caller must have checked that `new` descends from `old`. | |
| 146 | pub async fn fast_forward( | |
| 147 | source: &GitAccess, | |
| 148 | target: &GitAccess, | |
| 149 | branch: &str, | |
| 150 | old: Option<&str>, | |
| 151 | new: &str, | |
| 152 | ) -> Result<std::result::Result<(), String>> { | |
| 153 | let pack = fetch_pack(source, new, old).await?; | |
| 154 | push_pack(target, branch, old, new, pack).await | |
| 155 | } |