Skip to content
176 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.

The sidebar asks billing whether it is on, not for the account, so no page waits on the card processor1//! Brings a pull request up to date with the branch it would merge into.
2//!
3//! When that branch has moved, the pull request cannot merge until it holds
4//! the new commits. This merges them in. If the merge is clean that is all;
5//! if it conflicts, the agent is given the conflicted files and what the
6//! pull request is for, and resolves them. Either way the result is pushed
7//! and everything done is recorded in the pull request's session.
8//!
9//! Configuration, beyond what a session needs (see `main`):
10//!
11//! - `GIT_REMOTE`, `GIT_BRANCH`: the pull request's source.
12//! - `UPSTREAM_REMOTE`, `UPSTREAM_BRANCH`: what it would merge into.
13//! - `PROMPT`: what the pull request is for, given to the agent on a conflict.
14
15use std::path::Path;
16use std::process::Command;
17
18use anyhow::{Context, Result, bail};
19
20use crate::report::{Entry, Reporter};
21use crate::{WORKDIR, auth_option, env, git, harness};
22
23/// Runs git and says whether it succeeded, for commands whose failure is
24/// an answer and not an error.
25fn git_ok(dir: &Path, args: &[&str]) -> bool {
26 Command::new("git")
27 .current_dir(dir)
28 .args(args)
29 .output()
30 .is_ok_and(|output| output.status.success())
31}
32
33fn short(commit: &str) -> &str {
34 &commit[..commit.len().min(12)]
35}
36
37/// The files a stopped merge left in conflict.
38fn conflicted(workdir: &Path) -> Result<Vec<String>> {
39 Ok(git(workdir, &["diff", "--name-only", "--diff-filter=U"])?
40 .lines()
41 .map(str::to_owned)
42 .collect())
43}
44
45/// Whether a file still holds the markers git writes around a conflict.
46fn has_markers(workdir: &Path, file: &str) -> bool {
47 std::fs::read_to_string(workdir.join(file)).is_ok_and(|text| {
48 text.lines()
49 .any(|line| line.starts_with("<<<<<<< ") || line.starts_with(">>>>>>> "))
50 })
51}
52
53fn update(reporter: &mut Reporter) -> Result<()> {
54 let remote = env("GIT_REMOTE")?;
55 let branch = env("GIT_BRANCH")?;
56 let upstream = env("UPSTREAM_REMOTE")?;
57 let upstream_branch = env("UPSTREAM_BRANCH")?;
58 let auth = auth_option(&env("G1T_USER")?, &env("G1T_TOKEN")?);
59 let workdir = Path::new(WORKDIR);
60
61 std::fs::create_dir_all("/work")?;
62 crate::clone::clone(Path::new("/work"), &auth, &["--branch", &branch], &remote, WORKDIR).context("could not clone the pull request")?;
63 git(workdir, &["config", "user.name", crate::AUTHOR_NAME])?;
64 git(workdir, &["config", "user.email", crate::AUTHOR_EMAIL])?;
65 crate::clone::fetch(workdir, &auth, &upstream, &upstream_branch).with_context(|| format!("could not fetch {upstream_branch}"))?;
66 // Shallow: deep enough to tell whether it is behind, and to merge.
67 crate::clone::share_history(workdir, &auth, &[("origin", branch.as_str()), (upstream.as_str(), upstream_branch.as_str())], "HEAD", "FETCH_HEAD")?;
68 let theirs = git(workdir, &["rev-parse", "FETCH_HEAD"])?;
69
70 if git_ok(
71 workdir,
72 &["merge-base", "--is-ancestor", "FETCH_HEAD", "HEAD"],
73 ) {
74 reporter.record(Entry::new(
75 "note",
76 &format!("Already up to date with {upstream_branch}."),
77 ));
78 return Ok(());
79 }
80 reporter.record(Entry::new(
81 "note",
82 &format!(
83 "{upstream_branch} has moved to {}. Merging it into this pull request.",
84 short(&theirs)
85 ),
86 ));
87 reporter.flush();
88
89 let message = format!("Catch up with {upstream_branch}");
90 if !git_ok(
91 workdir,
92 &["merge", "--no-edit", "-m", &message, "FETCH_HEAD"],
93 ) {
94 let files = conflicted(workdir)?;
95 if files.is_empty() {
96 bail!("the merge failed for a reason other than a conflict");
97 }
98 reporter.record(Entry::new(
99 "note",
100 &format!("The merge conflicts in: {}.", files.join(", ")),
101 ));
102 let prompt = format!(
103 "You are a coding agent working in the git repository checked out in the current directory.\n\n\
104 A merge of `{upstream_branch}` into this pull request's branch has stopped with conflicts in:\n{}\n\n\
105 What this pull request is for:\n\n{}\n\n\
106 Resolve every conflict so that the result keeps what `{upstream_branch}` changed and what this pull request set out to do. \
107 Read both sides before choosing; do not simply take one. Remove all conflict markers. \
108 If the project has tests, run them. Then stage the files with `git add`. Do not commit and do not push; that is done for you. \
109 Finish with one or two sentences on how you resolved each conflict.",
110 files
111 .iter()
112 .map(|file| format!("- {file}"))
113 .collect::<Vec<_>>()
114 .join("\n"),
115 std::env::var("PROMPT").unwrap_or_default(),
116 );
117 reporter.record(Entry::new("prompt", &prompt));
118 reporter.flush();
119 let summary = harness::run_claude(workdir, &prompt, reporter)?;
120 reporter.record(Entry::new("message", &summary));
121
122 git(workdir, &["add", "--all"])?;
123 let unresolved = conflicted(workdir)?;
124 if !unresolved.is_empty() || files.iter().any(|file| has_markers(workdir, file)) {
125 bail!("conflicts remain after the agent's attempt to resolve them");
126 }
127 git(workdir, &["commit", "--quiet", "--no-edit"])
128 .context("could not conclude the merge")?;
129 reporter.record(Entry::new(
130 "note",
131 &format!("Resolved the conflicts in {}.", files.join(", ")),
132 ));
133 }
134
135 let head = git(workdir, &["rev-parse", "HEAD"])?;
136 git(
137 workdir,
138 &[
139 "-c",
140 &auth,
141 "push",
142 "--quiet",
143 "origin",
144 &format!("HEAD:{branch}"),
145 ],
146 )
147 .context("could not push the updated pull request")?;
148 reporter.record(Entry::new(
149 "note",
150 &format!(
151 "Pushed {}. This pull request now contains {upstream_branch}.",
152 short(&head)
153 ),
154 ));
155 Ok(())
156}
157
158pub fn main() -> i32 {
159 let mut reporter = match Reporter::from_env() {
160 Ok(reporter) => reporter,
161 Err(error) => {
162 eprintln!("g1t-runner: {error:#}");
163 return 2;
164 }
165 };
166 let outcome = update(&mut reporter);
167 if let Err(error) = &outcome {
168 eprintln!("g1t-runner: {error:#}");
169 reporter.record(Entry::new(
170 "note",
171 &format!("Catching up failed: {error:#}. Nothing was pushed."),
172 ));
173 }
174 reporter.flush();
175 i32::from(outcome.is_err())
176}

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