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.
| Initial g1t: services, event bus, intents and attempts | 1 | //! Bridges git's stateful SSH transport to the stateless smart HTTP protocol |
| 2 | //! spoken by Artifacts. | |
| 3 | ||
| 4 | use anyhow::{Result, bail, ensure}; | |
| 5 | use flate2::{Decompress, FlushDecompress, Status}; | |
| 6 | use futures_util::StreamExt; | |
| 7 | use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWrite, AsyncWriteExt}; | |
| 8 | ||
| 9 | use crate::api::{Access, Service}; | |
| 10 | ||
| 11 | const FLUSH: &[u8] = b"0000"; | |
| 12 | const ZERO_ID: &str = "0000000000000000000000000000000000000000"; | |
| 13 | ||
| 14 | /// A pkt-line carrying `ERR`, which git prints as `fatal: remote error: …`. | |
| 15 | pub fn error_pkt(message: &str) -> Vec<u8> { | |
| 16 | let payload = format!("ERR {message}\n"); | |
| 17 | format!("{:04x}{payload}", payload.len() + 4).into_bytes() | |
| 18 | } | |
| 19 | ||
| 20 | pub async fn serve<R, W>( | |
| 21 | http: &reqwest::Client, | |
| 22 | access: &Access, | |
| 23 | service: Service, | |
| 24 | protocol_v2: bool, | |
| 25 | reader: &mut R, | |
| 26 | writer: &mut W, | |
| 27 | ) -> Result<()> | |
| 28 | where | |
| 29 | R: AsyncBufRead + Unpin, | |
| 30 | W: AsyncWrite + Unpin, | |
| 31 | { | |
| 32 | let bridge = Bridge { | |
| 33 | http, | |
| 34 | access, | |
| 35 | service, | |
| 36 | // Artifacts only supports version 2 for fetches. | |
| 37 | protocol_v2: protocol_v2 && service == Service::UploadPack, | |
| 38 | }; | |
| 39 | match service { | |
| 40 | Service::UploadPack => bridge.upload_pack(reader, writer).await, | |
| 41 | Service::ReceivePack => bridge.receive_pack(reader, writer).await, | |
| 42 | } | |
| 43 | } | |
| 44 | ||
| 45 | struct Bridge<'a> { | |
| 46 | http: &'a reqwest::Client, | |
| 47 | access: &'a Access, | |
| 48 | service: Service, | |
| 49 | protocol_v2: bool, | |
| 50 | } | |
| 51 | ||
| 52 | impl Bridge<'_> { | |
| 53 | fn request(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder { | |
| 54 | let mut request = self | |
| 55 | .http | |
| 56 | .request(method, format!("{}/{path}", self.access.remote)) | |
| 57 | .bearer_auth(&self.access.token); | |
| 58 | if self.protocol_v2 { | |
| 59 | request = request.header("git-protocol", "version=2"); | |
| 60 | } | |
| 61 | request | |
| 62 | } | |
| 63 | ||
| 64 | /// Sends the ref advertisement, which over SSH comes without the | |
| 65 | /// `# service=…` section that smart HTTP prepends. | |
| 66 | async fn advertise<W: AsyncWrite + Unpin>(&self, writer: &mut W) -> Result<()> { | |
| 67 | let service = self.service.as_str(); | |
| 68 | let response = self | |
| API and MCP server, Rust identity service, registration, site redesign | 69 | .request( |
| 70 | reqwest::Method::GET, | |
| 71 | &format!("info/refs?service={service}"), | |
| 72 | ) | |
| Initial g1t: services, event bus, intents and attempts | 73 | .send() |
| 74 | .await? | |
| 75 | .error_for_status()?; | |
| 76 | let body = response.bytes().await?; | |
| 77 | let mut advertisement = &body[..]; | |
| API and MCP server, Rust identity service, registration, site redesign | 78 | if advertisement |
| 79 | .get(4..) | |
| 80 | .is_some_and(|rest| rest.starts_with(b"# service=")) | |
| 81 | { | |
| Initial g1t: services, event bus, intents and attempts | 82 | let length = usize::from_str_radix(std::str::from_utf8(&body[..4])?, 16)?; |
| 83 | ensure!(body.len() >= length + 4, "truncated ref advertisement"); | |
| 84 | advertisement = &body[length + 4..]; | |
| 85 | } | |
| 86 | writer.write_all(advertisement).await?; | |
| 87 | writer.flush().await?; | |
| 88 | Ok(()) | |
| 89 | } | |
| 90 | ||
| 91 | /// Posts one request and streams the response back to the client. | |
| 92 | async fn rpc<W: AsyncWrite + Unpin>(&self, body: Vec<u8>, writer: &mut W) -> Result<()> { | |
| 93 | let service = self.service.as_str(); | |
| 94 | let response = self | |
| 95 | .request(reqwest::Method::POST, service) | |
| 96 | .header("content-type", format!("application/x-{service}-request")) | |
| 97 | .header("accept", format!("application/x-{service}-result")) | |
| 98 | .body(body) | |
| 99 | .send() | |
| 100 | .await? | |
| 101 | .error_for_status()?; | |
| 102 | let mut stream = response.bytes_stream(); | |
| 103 | while let Some(chunk) = stream.next().await { | |
| 104 | writer.write_all(&chunk?).await?; | |
| 105 | } | |
| 106 | writer.flush().await?; | |
| 107 | Ok(()) | |
| 108 | } | |
| 109 | ||
| 110 | /// Protocol v2 is a series of self-contained commands, so each one maps | |
| 111 | /// onto a single HTTP request. | |
| 112 | async fn upload_pack<R, W>(&self, reader: &mut R, writer: &mut W) -> Result<()> | |
| 113 | where | |
| 114 | R: AsyncBufRead + Unpin, | |
| 115 | W: AsyncWrite + Unpin, | |
| 116 | { | |
| 117 | if !self.protocol_v2 { | |
| 118 | writer | |
| 119 | .write_all(&error_pkt( | |
| 120 | "g1t needs git protocol version 2 over SSH (git 2.26 or newer)", | |
| 121 | )) | |
| 122 | .await?; | |
| 123 | return Ok(()); | |
| 124 | } | |
| 125 | self.advertise(writer).await?; | |
| 126 | loop { | |
| 127 | let mut command = Vec::new(); | |
| 128 | if reader.fill_buf().await?.is_empty() { | |
| 129 | return Ok(()); | |
| 130 | } | |
| 131 | read_section(reader, &mut command).await?; | |
| 132 | if command == FLUSH { | |
| 133 | return Ok(()); | |
| 134 | } | |
| 135 | self.rpc(command, writer).await?; | |
| 136 | } | |
| 137 | } | |
| 138 | ||
| 139 | async fn receive_pack<R, W>(&self, reader: &mut R, writer: &mut W) -> Result<()> | |
| 140 | where | |
| 141 | R: AsyncBufRead + Unpin, | |
| 142 | W: AsyncWrite + Unpin, | |
| 143 | { | |
| 144 | self.advertise(writer).await?; | |
| 145 | if reader.fill_buf().await?.is_empty() { | |
| 146 | return Ok(()); | |
| 147 | } | |
| 148 | let mut body = Vec::new(); | |
| 149 | let lines = read_section(reader, &mut body).await?; | |
| 150 | let commands: Vec<&str> = lines | |
| 151 | .iter() | |
| 152 | .filter(|line| !line.starts_with("shallow ")) | |
| 153 | .map(String::as_str) | |
| 154 | .collect(); | |
| 155 | // A bare flush means the client had nothing to push. | |
| 156 | let Some(first) = commands.first() else { | |
| 157 | return Ok(()); | |
| 158 | }; | |
| 159 | let capabilities = first.split_once('\0').map_or("", |(_, caps)| caps); | |
| 160 | let has_push_options = capabilities.split(' ').any(|cap| cap == "push-options"); | |
| 161 | // Commands are "<old id> <new id> <ref>"; only deletions send no pack. | |
| 162 | let sends_pack = commands | |
| 163 | .iter() | |
| 164 | .any(|command| command.split(' ').nth(1) != Some(ZERO_ID)); | |
| 165 | ||
| 166 | if has_push_options { | |
| 167 | read_section(reader, &mut body).await?; | |
| 168 | } | |
| 169 | if sends_pack { | |
| 170 | read_pack(reader, &mut body).await?; | |
| 171 | } | |
| 172 | self.rpc(body, writer).await | |
| 173 | } | |
| 174 | } | |
| 175 | ||
| 176 | /// Reads pkt-lines up to and including the next flush, appending the raw | |
| 177 | /// bytes to `raw`. Returns the payload of each line. | |
| 178 | async fn read_section<R: AsyncBufRead + Unpin>( | |
| 179 | reader: &mut R, | |
| 180 | raw: &mut Vec<u8>, | |
| 181 | ) -> Result<Vec<String>> { | |
| 182 | let mut lines = Vec::new(); | |
| 183 | loop { | |
| 184 | let mut header = [0u8; 4]; | |
| 185 | reader.read_exact(&mut header).await?; | |
| 186 | raw.extend_from_slice(&header); | |
| 187 | let length = usize::from_str_radix(std::str::from_utf8(&header)?, 16)?; | |
| 188 | match length { | |
| 189 | 0 => return Ok(lines), | |
| 190 | // Delimiter and response-end packets carry no payload. | |
| 191 | 1 | 2 => continue, | |
| 192 | 3 => bail!("invalid pkt-line length"), | |
| 193 | _ => { | |
| 194 | let mut payload = vec![0u8; length - 4]; | |
| 195 | reader.read_exact(&mut payload).await?; | |
| 196 | raw.extend_from_slice(&payload); | |
| 197 | lines.push(String::from_utf8_lossy(&payload).trim_end().to_owned()); | |
| 198 | } | |
| 199 | } | |
| 200 | } | |
| 201 | } | |
| 202 | ||
| 203 | async fn read_into<R: AsyncBufRead + Unpin>( | |
| 204 | reader: &mut R, | |
| 205 | raw: &mut Vec<u8>, | |
| 206 | count: usize, | |
| 207 | ) -> Result<()> { | |
| 208 | let start = raw.len(); | |
| 209 | raw.resize(start + count, 0); | |
| 210 | reader.read_exact(&mut raw[start..]).await?; | |
| 211 | Ok(()) | |
| 212 | } | |
| 213 | ||
| 214 | async fn read_byte<R: AsyncBufRead + Unpin>(reader: &mut R, raw: &mut Vec<u8>) -> Result<u8> { | |
| 215 | let byte = reader.read_u8().await?; | |
| 216 | raw.push(byte); | |
| 217 | Ok(byte) | |
| 218 | } | |
| 219 | ||
| 220 | /// Reads exactly one packfile. Over SSH nothing marks where the pack ends | |
| 221 | /// (the client keeps the channel open for the reply), so the only way to | |
| 222 | /// find the end is to walk every object. | |
| 223 | async fn read_pack<R: AsyncBufRead + Unpin>(reader: &mut R, raw: &mut Vec<u8>) -> Result<()> { | |
| 224 | const OFS_DELTA: u8 = 6; | |
| 225 | const REF_DELTA: u8 = 7; | |
| 226 | ||
| 227 | let start = raw.len(); | |
| 228 | read_into(reader, raw, 12).await?; | |
| 229 | ensure!(&raw[start..start + 4] == b"PACK", "expected a packfile"); | |
| 230 | let objects = u32::from_be_bytes(raw[start + 8..start + 12].try_into()?); | |
| 231 | ||
| 232 | let mut scratch = vec![0u8; 64 * 1024]; | |
| 233 | for _ in 0..objects { | |
| 234 | // Type and size header: a varint whose first byte holds the type. | |
| 235 | let mut byte = read_byte(reader, raw).await?; | |
| 236 | let kind = (byte >> 4) & 7; | |
| 237 | while byte & 0x80 != 0 { | |
| 238 | byte = read_byte(reader, raw).await?; | |
| 239 | } | |
| 240 | match kind { | |
| API and MCP server, Rust identity service, registration, site redesign | 241 | OFS_DELTA => while read_byte(reader, raw).await? & 0x80 != 0 {}, |
| Initial g1t: services, event bus, intents and attempts | 242 | REF_DELTA => read_into(reader, raw, 20).await?, |
| 243 | _ => {} | |
| 244 | } | |
| 245 | ||
| 246 | let mut inflate = Decompress::new(true); | |
| 247 | loop { | |
| 248 | let input = reader.fill_buf().await?; | |
| 249 | ensure!(!input.is_empty(), "packfile ended mid-object"); | |
| 250 | let before = inflate.total_in(); | |
| 251 | let status = inflate.decompress(input, &mut scratch, FlushDecompress::None)?; | |
| 252 | let consumed = (inflate.total_in() - before) as usize; | |
| 253 | raw.extend_from_slice(&input[..consumed]); | |
| 254 | reader.consume(consumed); | |
| 255 | if status == Status::StreamEnd { | |
| 256 | break; | |
| 257 | } | |
| 258 | } | |
| 259 | } | |
| 260 | // Trailing SHA-1 checksum. | |
| 261 | read_into(reader, raw, 20).await | |
| 262 | } |