Skip to content

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.sh
244 linesCodeBlameRaw

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.

A last-commits walk reads history 48 commits at a time, within its budget1//! Builds one state of a merge queue: the default branch with a run of pull
2//! requests merged in, in queue order. The state is pushed to its own branch,
3//! where the repository's `merge_group` workflows check it,
4//! from which g1t lands it if it passed and everything ahead of it has
5//! landed.
6//!
The runner no longer runs g1t's own agents. Cloud and self-hosted runners now do what a forge's runner does: Actions jobs, required checks, merge checks and merge-queue test runs, deployments, dependency bumps and backups. Everything that started Claude Code in a sandbox on an issue or pull request is gone, with its prompts, model access, mentions, delegation, plans, reviews, revisions and the workspace agents' computers.7//! A merge that does not apply cleanly is reported as a conflict, naming
8//! the pull request ahead whose change it collided with; resolving it is
9//! for the pull request's author once that one has landed.
A last-commits walk reads history 48 commits at a time, within its budget10//!
11//! Configuration comes from the environment:
12//!
13//! - `G1T_API`, `QUEUE_ENTRY`, `QUEUE_TOKEN`: where and how to report.
14//! - `G1T_USER`, `G1T_TOKEN`: a member, to read the changes and push.
15//! - `BASE_REMOTE`, `BASE_COMMIT`: the repository and the commit to build on.
16//! - `QUEUE_BRANCH`: where to push the tested state.
17//! - `STACK`: the pull requests to merge, as JSON `[{number, title, remote,
18//! branch, commit}]`, the entry being tested last.
19//! - `CHECKS`: commands to run on the state, as a JSON array. g1t sends
20//! none now: the state is checked by its `merge_group` workflows.
21//! - `CONTRACT_CHECKS`: the checks of issues already completed, which the
22//! default branch has to keep passing. One that fails is run again on the
23//! base alone; if it fails there too, it was broken already and is not
24//! held against the stack.
25
26use std::path::Path;
27use std::process::Command;
28
29use anyhow::{Context, Result, bail};
30use serde::Deserialize;
31
32use crate::checks::{redact, run_command};
33use crate::{WORKDIR, auth_option, env, git};
34
35#[derive(Deserialize)]
36struct Item {
37 number: u32,
38 title: String,
39 remote: String,
40 branch: String,
41 commit: String,
42}
43
44/// What stopped a state from being built.
45enum Stopped {
46 /// Merging this pull request's change did not apply cleanly. The
47 /// pull request ahead whose change it collided with, if one did.
48 Conflict { number: u32, with: Option<u32>, files: Vec<String> },
49 Failed(anyhow::Error),
50}
51
52fn git_ok(dir: &Path, args: &[&str]) -> bool {
53 Command::new("git")
54 .current_dir(dir)
55 .args(args)
56 .output()
57 .is_ok_and(|output| output.status.success())
58}
59
60fn changed_files(dir: &Path, from: &str, to: &str) -> Vec<String> {
61 git(dir, &["diff", "--name-only", from, to])
62 .map(|out| out.lines().map(str::to_owned).collect())
63 .unwrap_or_default()
64}
65
66/// Clones the base, then merges each pull request in order. Returns the
67/// tested state's commit.
68fn build(stack: &[Item], auth: &str) -> std::result::Result<String, Stopped> {
69 let base_remote = env("BASE_REMOTE").map_err(Stopped::Failed)?;
70 let base = env("BASE_COMMIT").map_err(Stopped::Failed)?;
71 std::fs::create_dir_all("/work").map_err(|error| Stopped::Failed(error.into()))?;
72 let workdir = Path::new(WORKDIR);
73 crate::clone::clone(Path::new("/work"), auth, &[], &base_remote, WORKDIR)
74 .and_then(|_| git(workdir, &["rev-parse", "--abbrev-ref", "HEAD"]))
75 .and_then(|branch| crate::clone::ensure(workdir, auth, "origin", &branch, &base).map(|_| branch))
76 .and_then(|_| git(workdir, &["checkout", "--quiet", "-B", "g1t-queue", &base]))
77 .and_then(|_| git(workdir, &["config", "user.name", crate::AUTHOR_NAME]))
78 .and_then(|_| git(workdir, &["config", "user.email", crate::AUTHOR_EMAIL]))
79 .map_err(Stopped::Failed)?;
80
81 for (index, item) in stack.iter().enumerate() {
82 crate::clone::fetch(workdir, auth, &item.remote, &item.branch)
83 .with_context(|| format!("could not fetch #{}", item.number))
84 .map_err(Stopped::Failed)?;
85 let _ = crate::clone::ensure(workdir, auth, &item.remote, &item.branch, &item.commit);
86 // Shallow: deep enough for the change and the state so far to share
87 // a commit to merge from.
88 if crate::clone::has(workdir, &item.commit) {
89 crate::clone::share_history(workdir, auth, &[(base_remote.as_str(), "HEAD"), (item.remote.as_str(), item.branch.as_str())], "HEAD", &item.commit)
90 .map_err(Stopped::Failed)?;
91 }
92 // The branch may have moved on since the queue looked; what was
93 // queued is the commit, and it must be there.
94 if !git_ok(workdir, &["cat-file", "-e", &format!("{}^{{commit}}", item.commit)]) {
95 return Err(Stopped::Failed(anyhow::anyhow!(
96 "#{}'s commit {} is no longer on its branch",
97 item.number,
98 &item.commit[..item.commit.len().min(12)]
99 )));
100 }
101 let message = format!("Merge #{}: {}", item.number, item.title);
102 if !git_ok(workdir, &["merge", "--quiet", "--no-edit", "-m", &message, &item.commit]) {
103 let files: Vec<String> = git(workdir, &["diff", "--name-only", "--diff-filter=U"])
104 .map(|out| out.lines().map(str::to_owned).collect())
105 .unwrap_or_default();
106 let _ = git(workdir, &["merge", "--abort"]);
107 // The pull request ahead that changed one of the same files.
108 let with = stack[..index]
109 .iter()
110 .rev()
111 .find(|earlier| {
112 let theirs = changed_files(workdir, &base, &earlier.commit);
113 files.iter().any(|file| theirs.contains(file))
114 })
115 .map(|earlier| earlier.number);
116 return Err(Stopped::Conflict {
117 number: item.number,
118 with,
119 files,
120 });
121 }
122 }
123 git(workdir, &["rev-parse", "HEAD"])
124 .map(|out| out.trim().to_owned())
125 .map_err(Stopped::Failed)
126}
127
128fn report(api: &str, entry: &str, token: &str, mut body: serde_json::Value) -> Result<()> {
129 body["token"] = token.into();
130 ureq::post(&format!("{api}/queue/{entry}"))
131 .send_json(body)
132 .context("could not report the merge queue's result")?;
133 Ok(())
134}
135
136pub fn main() -> i32 {
137 let (api, entry, token) = match (env("G1T_API"), env("QUEUE_ENTRY"), env("QUEUE_TOKEN")) {
138 (Ok(api), Ok(entry), Ok(token)) => (api, entry, token),
139 _ => {
140 eprintln!("g1t-runner: G1T_API, QUEUE_ENTRY and QUEUE_TOKEN must be set");
141 return 2;
142 }
143 };
144 let secrets: Vec<String> = ["G1T_TOKEN", "QUEUE_TOKEN"]
145 .iter()
146 .filter_map(|name| std::env::var(name).ok())
147 .filter(|secret| !secret.is_empty())
148 .collect();
149 let result = (|| -> Result<serde_json::Value> {
150 let auth = auth_option(&env("G1T_USER")?, &env("G1T_TOKEN")?);
151 let stack: Vec<Item> = serde_json::from_str(&env("STACK")?).context("STACK is not valid")?;
152 if stack.is_empty() {
153 bail!("nothing to merge");
154 }
155 let commands: Vec<String> = std::env::var("CHECKS")
156 .ok()
157 .and_then(|json| serde_json::from_str(&json).ok())
158 .unwrap_or_default();
159 let combined = match build(&stack, &auth) {
160 Ok(combined) => combined,
161 Err(Stopped::Conflict { number, with, files }) => {
162 let against = with.map_or("the default branch".to_owned(), |n| format!("#{n}"));
163 return Ok(serde_json::json!({
164 "error": format!(
165 "#{number} does not merge cleanly with {against}: {} conflict.",
166 files.join(", ")
167 ),
168 "conflictWith": with,
169 "conflicts": files,
170 }));
171 }
172 Err(Stopped::Failed(error)) => {
173 return Ok(serde_json::json!({
174 "error": redact(&format!("{error:#}"), &secrets),
175 }));
176 }
177 };
178 let contract: Vec<String> = std::env::var("CONTRACT_CHECKS")
179 .ok()
180 .and_then(|json| serde_json::from_str(&json).ok())
181 .unwrap_or_default();
182 let workdir = Path::new(WORKDIR);
183 let mut results: Vec<_> = commands
184 .iter()
185 .map(|command| run_command(command, workdir, &secrets))
186 .collect();
187 let contract_results: Vec<_> = contract
188 .iter()
189 .map(|command| run_command(command, workdir, &secrets))
190 .collect();
191 // A contract check that fails here may have been failing on the base
192 // already: try those on the base alone.
193 let failing: Vec<usize> = (0..contract_results.len())
194 .filter(|&index| !contract_results[index].passed)
195 .collect();
196 let mut contract_results = contract_results;
197 if !failing.is_empty() {
198 let base = env("BASE_COMMIT")?;
199 let combined_head = git(workdir, &["rev-parse", "HEAD"])?.trim().to_owned();
200 git(workdir, &["checkout", "--quiet", "--detach", &base])?;
201 for index in failing {
202 let on_base = run_command(&contract_results[index].command, workdir, &secrets);
203 if !on_base.passed {
204 let result = &mut contract_results[index];
205 result.passed = true;
206 result.command = format!(
207 "{} (already failing on the default branch; not held against this)",
208 result.command
209 );
210 }
211 }
212 git(workdir, &["checkout", "--quiet", &combined_head])?;
213 }
214 results.extend(contract_results);
215 // Pushed whether or not it passed, so a failure can be looked at.
216 let branch = env("QUEUE_BRANCH")?;
217 let base_remote = env("BASE_REMOTE")?;
218 git(
219 Path::new(WORKDIR),
220 &[
221 "-c",
222 &auth,
223 "push",
224 "--quiet",
225 "--force",
226 &base_remote,
227 &format!("HEAD:refs/heads/{branch}"),
228 ],
229 )
230 .map_err(|error| anyhow::anyhow!("{}", redact(&format!("{error:#}"), &secrets)))
231 .context("could not push the tested state")?;
232 Ok(serde_json::json!({ "combinedCommit": combined, "results": results }))
233 })();
234 let body = result.unwrap_or_else(|error| {
235 serde_json::json!({ "error": redact(&format!("{error:#}"), &secrets) })
236 });
237 match report(&api, &entry, &token, body) {
238 Ok(()) => 0,
239 Err(error) => {
240 eprintln!("g1t-runner: {error:#}");
241 1
242 }
243 }
244}

This file's history is long; its oldest lines are credited to the oldest commit read.