Skip to content

Commit ac61164

Browse files
committed
Implement push
1 parent 0453b07 commit ac61164

File tree

4 files changed

+198
-22
lines changed

4 files changed

+198
-22
lines changed

src/bin/josh_sync.rs

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use anyhow::Context;
22
use clap::Parser;
33
use josh_sync::config::{JoshConfig, load_config};
44
use josh_sync::josh::{JoshProxy, try_install_josh};
5-
use josh_sync::sync::{GitSync, RustcPullError};
5+
use josh_sync::sync::{GitSync, RustcPullError, UPSTREAM_REPO};
66
use josh_sync::utils::prompt;
77
use std::path::{Path, PathBuf};
88

@@ -18,12 +18,23 @@ struct Args {
1818
enum Command {
1919
/// Initialize a config file for this repository.
2020
Init,
21-
/// Pull changes from the main `rustc` repository.
21+
/// Pull changes from the main `rust-lang/rust` repository.
2222
/// This creates new commits that should be then merged into this subtree repository.
2323
Pull {
2424
#[clap(long, default_value(DEFAULT_CONFIG_PATH))]
2525
config: PathBuf,
2626
},
27+
/// Push changes into the main `rust-lang/rust` repository `branch` of a `rustc` fork under
28+
/// the given GitHub `username`.
29+
/// The pushed branch should then be merged into the `rustc` repository.
30+
Push {
31+
#[clap(long, default_value(DEFAULT_CONFIG_PATH))]
32+
config: PathBuf,
33+
/// Branch that should be pushed to your remote
34+
branch: String,
35+
/// Your GitHub usename where the fork is located
36+
username: String,
37+
},
2738
}
2839

2940
fn main() -> anyhow::Result<()> {
@@ -48,7 +59,11 @@ fn main() -> anyhow::Result<()> {
4859
let sync = GitSync::new(config.clone(), josh);
4960
match sync.rustc_pull() {
5061
Ok(result) => {
51-
maybe_create_gh_pr(&config.config, &result.merge_commit_message)?;
62+
maybe_create_gh_pr(
63+
&config.config.full_repo_name(),
64+
"Rustc pull update",
65+
&result.merge_commit_message,
66+
)?;
5267
}
5368
Err(RustcPullError::NothingToPull) => {
5469
eprintln!("Nothing to pull");
@@ -60,35 +75,55 @@ fn main() -> anyhow::Result<()> {
6075
}
6176
}
6277
}
78+
Command::Push {
79+
username,
80+
branch,
81+
config,
82+
} => {
83+
let config = load_config(&config)
84+
.context("cannot load config. Run the `init` command to initialize it.")?;
85+
let josh = get_josh_proxy()?;
86+
let sync = GitSync::new(config.clone(), josh);
87+
sync.rustc_push(&username, &branch)
88+
.context("cannot perform push")?;
89+
90+
// Open PR with `subtree update` title to silence the `no-merges` triagebot check
91+
println!(
92+
r#"You can create the rustc PR using the following URL:
93+
https://github.com/{UPSTREAM_REPO}/compare/{username}:{branch}?quick_pull=1&title={}+subtree+update&body=r?+@ghost"#,
94+
config.config.repo
95+
);
96+
}
6397
}
6498

6599
Ok(())
66100
}
67101

68-
fn maybe_create_gh_pr(config: &JoshConfig, pr_description: &str) -> anyhow::Result<()> {
102+
fn maybe_create_gh_pr(repo: &str, title: &str, description: &str) -> anyhow::Result<bool> {
69103
let gh_available = which::which("gh").is_ok();
70104
if !gh_available {
71105
println!(
72106
"Note: if you install the `gh` CLI tool, josh-sync will be able to create the sync PR for you."
73107
);
108+
Ok(false)
74109
} else if prompt("Do you want to create a rustc pull PR using the `gh` tool?") {
75-
let repo = config.full_repo_name();
76110
std::process::Command::new("gh")
77111
.args(&[
78112
"pr",
79113
"create",
80114
"--title",
81-
"Rustc pull update",
115+
title,
82116
"--body",
83-
pr_description,
117+
description,
84118
"--repo",
85-
&repo,
119+
repo,
86120
])
87121
.spawn()?
88122
.wait()?;
123+
Ok(true)
124+
} else {
125+
Ok(false)
89126
}
90-
91-
Ok(())
92127
}
93128

94129
fn get_josh_proxy() -> anyhow::Result<JoshProxy> {

src/josh.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,10 @@ pub struct RunningJoshProxy {
8282
}
8383

8484
impl RunningJoshProxy {
85-
pub fn git_url(&self, upstream_repo: &str, commit: &str, filter: &str) -> String {
85+
pub fn git_url(&self, repo: &str, commit: Option<&str>, filter: &str) -> String {
86+
let commit = commit.map(|c| format!("@{c}")).unwrap_or_default();
8687
format!(
87-
"http://localhost:{}/{upstream_repo}.git@{commit}{filter}.git",
88+
"http://localhost:{}/{repo}.git{commit}{filter}.git",
8889
self.port
8990
)
9091
}

src/sync.rs

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use crate::config::JoshConfigWithPath;
22
use crate::josh::JoshProxy;
3-
use crate::utils::{check_output, ensure_clean_git_state};
3+
use crate::utils::{check_output, check_output_at, ensure_clean_git_state};
44
use anyhow::{Context, Error};
5+
use std::path::{Path, PathBuf};
56

6-
const UPSTREAM_REPO: &str = "rust-lang/rust";
7+
pub const UPSTREAM_REPO: &str = "rust-lang/rust";
78

89
pub enum RustcPullError {
910
/// No changes are available to be pulled.
@@ -62,7 +63,7 @@ impl GitSync {
6263
.context("cannot start josh-proxy")?;
6364
let josh_url = josh.git_url(
6465
UPSTREAM_REPO,
65-
&upstream_sha,
66+
Some(&upstream_sha),
6667
&construct_filter(&self.config.config.path),
6768
);
6869

@@ -178,6 +179,131 @@ Filtered ref: {incoming_ref}
178179
merge_commit_message: merge_message,
179180
})
180181
}
182+
183+
pub fn rustc_push(&self, username: &str, branch: &str) -> anyhow::Result<()> {
184+
ensure_clean_git_state();
185+
186+
let base_upstream_sha = self
187+
.config
188+
.config
189+
.last_upstream_sha
190+
.clone()
191+
.unwrap_or_default();
192+
193+
// Make sure josh is running.
194+
let josh = self
195+
.proxy
196+
.start(&self.config.config)
197+
.context("cannot start josh-proxy")?;
198+
let josh_url = josh.git_url(
199+
&format!("{username}/rust"),
200+
None,
201+
&construct_filter(&self.config.config.path),
202+
);
203+
let user_upstream_url = format!("https://github.com/{username}/rust");
204+
205+
let rustc_git = prepare_rustc_checkout().context("cannot prepare rustc checkout")?;
206+
207+
// Prepare the branch. Pushing works much better if we use as base exactly
208+
// the commit that we pulled from last time, so we use the `rust-version`
209+
// file to find out which commit that would be.
210+
println!("Preparing {user_upstream_url} (base: {base_upstream_sha})...");
211+
212+
// Check if the remote branch doesn't already exist
213+
if check_output_at(
214+
&["git", "fetch", &user_upstream_url, branch],
215+
&rustc_git,
216+
true,
217+
)
218+
.is_ok()
219+
{
220+
return Err(anyhow::anyhow!(
221+
"The branch '{branch}' seems to already exist in '{user_upstream_url}'. Please delete it and try again."
222+
));
223+
}
224+
225+
// Download the base upstream SHA
226+
check_output_at(
227+
&[
228+
"git",
229+
"fetch",
230+
&format!("https://github.com/{UPSTREAM_REPO}"),
231+
&base_upstream_sha,
232+
],
233+
&rustc_git,
234+
false,
235+
)
236+
.context("cannot download latest upstream SHA")?;
237+
238+
// And push it to the user's fork's branch
239+
check_output_at(
240+
&[
241+
"git",
242+
"push",
243+
&user_upstream_url,
244+
&format!("{base_upstream_sha}:refs/heads/{branch}"),
245+
],
246+
&rustc_git,
247+
true,
248+
)
249+
.context("cannot push to your fork")?;
250+
println!();
251+
252+
// Do the actual push from the subtree git repo
253+
println!("Pushing changes...");
254+
check_output(&["git", "push", &josh_url, &format!("HEAD:{branch}")])?;
255+
println!();
256+
257+
// Do a round-trip check to make sure the push worked as expected.
258+
check_output_at(
259+
&["git", "fetch", &josh_url, &branch],
260+
&std::env::current_dir().unwrap(),
261+
true,
262+
)?;
263+
let head = check_output(&["git", "rev-parse", "HEAD"])?;
264+
let fetch_head = check_output(&["git", "rev-parse", "FETCH_HEAD"])?;
265+
if head != fetch_head {
266+
return Err(anyhow::anyhow!(
267+
"Josh created a non-roundtrip push! Do NOT merge this into rustc!\n\
268+
Expected {head}, got {fetch_head}."
269+
));
270+
}
271+
println!(
272+
"Confirmed that the push round-trips back to {} properly. Please create a rustc PR.",
273+
self.config.config.repo
274+
);
275+
276+
Ok(())
277+
}
278+
}
279+
280+
/// Find a rustc repo we can do our push preparation in.
281+
fn prepare_rustc_checkout() -> anyhow::Result<PathBuf> {
282+
if let Ok(rustc_git) = std::env::var("RUSTC_GIT") {
283+
let rustc_git = PathBuf::from(rustc_git);
284+
assert!(
285+
rustc_git.is_dir(),
286+
"rustc checkout path must be a directory"
287+
);
288+
return Ok(rustc_git);
289+
};
290+
291+
// Otherwise, download it
292+
let path = "rustc-checkout";
293+
if !Path::new(path).join(".git").exists() {
294+
println!(
295+
"Cloning rustc into `{path}`. Use RUSTC_GIT environment variable to override the location of the checkout"
296+
);
297+
check_output(&[
298+
"git",
299+
"clone",
300+
"--filter=blob:none",
301+
"https://github.com/rust-lang/rust",
302+
path,
303+
])
304+
.context("cannot clone rustc")?;
305+
}
306+
Ok(PathBuf::from(path))
181307
}
182308

183309
/// Removes the last commit on drop, unless `disarm` is called first.

src/utils.rs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,36 @@
1+
use std::path::Path;
12
use std::process::{Command, Stdio};
23

3-
/// Run a command from an array, collecting its output.
4-
pub fn check_output<'a, Args: AsRef<[&'a str]>>(l: Args) -> anyhow::Result<String> {
5-
let args = l.as_ref();
4+
/// Run command and return its stdout.
5+
pub fn check_output<'a, Args: AsRef<[&'a str]>>(args: Args) -> anyhow::Result<String> {
6+
check_output_at(args, &std::env::current_dir()?, false)
7+
}
8+
9+
pub fn check_output_at<'a, Args: AsRef<[&'a str]>>(
10+
args: Args,
11+
workdir: &Path,
12+
ignore_stderr: bool,
13+
) -> anyhow::Result<String> {
14+
let args = args.as_ref();
615

716
let mut cmd = Command::new(args[0]);
17+
cmd.current_dir(workdir);
818
cmd.args(&args[1..]);
9-
cmd.stderr(Stdio::inherit());
19+
20+
if ignore_stderr {
21+
cmd.stderr(Stdio::null());
22+
}
1023
eprintln!("+ {cmd:?}");
1124
let out = cmd.output().expect("command failed");
1225
let stdout = String::from_utf8_lossy(out.stdout.trim_ascii()).to_string();
1326
if !out.status.success() {
14-
panic!(
27+
Err(anyhow::anyhow!(
1528
"Command `{cmd:?}` failed with exit code {:?}. STDOUT:\n{stdout}",
1629
out.status.code()
17-
);
30+
))
31+
} else {
32+
Ok(stdout)
1833
}
19-
Ok(stdout)
2034
}
2135

2236
/// Fail if there are files that need to be checked in.

0 commit comments

Comments
 (0)