flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/services/repos/src/land.rs

155 lines5,743 bytesCodeBlame

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 attempts1//! Landing a pull request: moving a repository's branch forward to a commit
Pull requests from branches2//! from a fork, or from another of its own branches.
Rust repos service with shipping; pull requests kept in the model3//!
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
9use g1t_contracts::repos::GitAccess;
10use worker::js_sys::Uint8Array;
11use worker::{Error, Fetch, Headers, Method, Request, RequestInit, Result};
12
13const ZERO_ID: &str = "0000000000000000000000000000000000000000";
14const FLUSH: &[u8] = b"0000";
15/// Side-band channels: pack data, and fatal errors.
16const PACK_BAND: u8 = 1;
17const ERROR_BAND: u8 = 3;
18
19fn pkt_line(payload: &str) -> Vec<u8> {
20 format!("{:04x}{payload}", payload.len() + 4).into_bytes()
21}
22
23async 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 branches46pub(crate) fn read_pkt_lines(bytes: &[u8]) -> (Vec<&[u8]>, usize) {
Rust repos service with shipping; pull requests kept in the model47 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`.
70async 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 shell82 unpack_sideband(&response)
83}
84
85/// The pack in an upload-pack response that used side-band framing.
86pub(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 model88 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 branches94 "the source refused the fetch: {}",
Rust repos service with shipping; pull requests kept in the model95 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 branches104 "the source did not send a pack: {}",
Agents as a team: lifecycle, merge queue, billing and a new shell105 String::from_utf8_lossy(response)
Rust repos service with shipping; pull requests kept in the model106 )));
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 shell114pub(crate) async fn push_pack(
Rust repos service with shipping; pull requests kept in the model115 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`.
146pub 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}