diff --git a/josh-sync.example.toml b/josh-sync.example.toml index 998005b..ece01db 100644 --- a/josh-sync.example.toml +++ b/josh-sync.example.toml @@ -15,3 +15,16 @@ path = "library/stdarch" #[[post-pull]] #cmd = ["cargo", "fmt"] #commit-message = "reformat" + +# Optionally, you can specify a subtree filter. +# This will be applied to the local `HEAD` during the round-trip check. +# +# By default, the round-trip check expects the local repo to be a perfect 1:1 mirror +# of the upstream tree. Leave this empty if that is your intent. +# +# If you are not doing a 1:1 mirror, set this to a filter that transforms your local +# `HEAD` into a tree that exactly matches the upstream tree after the `filter` +# is applied. +# E.g., if the `filter` is ":/compiler/rustc_public:prefix=rustc_public", +# the `subtree-filter` should be: +#subtree-filter = ":/rustc_public:prefix=rustc_public" diff --git a/src/bin/rustc_josh_sync.rs b/src/bin/rustc_josh_sync.rs index 2c8a05a..f149ba7 100644 --- a/src/bin/rustc_josh_sync.rs +++ b/src/bin/rustc_josh_sync.rs @@ -87,6 +87,7 @@ fn main() -> anyhow::Result<()> { path: Some("".to_string()), filter: None, post_pull: vec![], + subtree_filter: None, }; config .write(Path::new(DEFAULT_CONFIG_PATH)) diff --git a/src/config.rs b/src/config.rs index ccbd9bd..748e1fe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,6 +17,9 @@ pub struct JoshConfig { /// Can be used to post-process the state of the repository after a pull happens. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub post_pull: Vec, + /// Optional subtree filter applied to the local `HEAD` during round-trip check. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub subtree_filter: Option, } /// Execute an operation after a pull, and if something changes in the local git state, diff --git a/src/josh.rs b/src/josh.rs index da7cf52..20e2d71 100644 --- a/src/josh.rs +++ b/src/josh.rs @@ -1,8 +1,8 @@ use crate::config::JoshConfig; -use crate::utils::run_command; +use crate::utils::{is_null_sha, run_command, run_command_by_path}; use anyhow::Context; use std::net::{SocketAddr, TcpStream}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; use std::time::Duration; @@ -14,6 +14,10 @@ pub struct JoshProxy { path: PathBuf, } +pub struct JoshFilter { + path: PathBuf, +} + impl JoshProxy { /// Tries to figure out if `josh-proxy` is installed. pub fn lookup() -> Option { @@ -82,6 +86,48 @@ pub fn try_install_josh(verbose: bool) -> Option { JoshProxy::lookup() } +impl JoshFilter { + /// Tries to figure out if `josh-filter` is installed. + pub fn lookup() -> Option { + which::which("josh-filter").ok().map(|path| Self { path }) + } + + pub fn run<'a, Args: AsRef<[&'a str]>>( + &self, + args: Args, + workdir: &Path, + verbose: bool, + ) -> anyhow::Result<()> { + let args = args.as_ref(); + let output = run_command_by_path(&self.path, args, workdir, true, verbose)?; + if is_null_sha(&output) { + return Err(anyhow::anyhow!( + "josh-filter returned null SHA, filter may not match any content" + )); + } + Ok(()) + } +} + +pub fn try_install_josh_filter(verbose: bool) -> Option { + run_command( + &[ + "cargo", + "+stable", + "install", + "--locked", + "--git", + "https://github.com/josh-project/josh", + "--tag", + JOSH_VERSION, + "josh-filter", + ], + verbose, + ) + .expect("cannot install josh-filter"); + JoshFilter::lookup() +} + /// Create a wrapper that represents a running instance of `josh-proxy` and stops it on drop. pub struct RunningJoshProxy { process: std::process::Child, diff --git a/src/sync.rs b/src/sync.rs index f9f0d26..f555176 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,6 +1,6 @@ use crate::SyncContext; -use crate::config::PostPullOperation; -use crate::josh::JoshProxy; +use crate::config::{JoshConfig, PostPullOperation}; +use crate::josh::{JoshFilter, JoshProxy, try_install_josh_filter}; use crate::utils::{ensure_clean_git_state, prompt}; use crate::utils::{get_current_head_sha, run_command_at}; use crate::utils::{run_command, stream_command}; @@ -329,23 +329,7 @@ After you fix the conflicts, `git add` the changes and run `git merge --continue println!(); // Do a round-trip check to make sure the push worked as expected. - run_command_at( - &["git", "fetch", &josh_url, &branch], - &std::env::current_dir().unwrap(), - self.verbose, - )?; - let head = get_current_head_sha(self.verbose)?; - let fetch_head = run_command(&["git", "rev-parse", "FETCH_HEAD"], self.verbose)?; - if head != fetch_head { - return Err(anyhow::anyhow!( - "Josh created a non-roundtrip push! Do NOT merge this into rustc!\n\ - Expected {head}, got {fetch_head}." - )); - } - println!( - "Confirmed that the push round-trips back to {} properly. Please create a rustc PR.", - self.context.config.repo - ); + self.roundtrip_check(&self.context.config, &josh_url, &branch)?; Ok(()) } @@ -370,6 +354,51 @@ After you fix the conflicts, `git add` the changes and run `git merge --continue Ok(()) } + + fn roundtrip_check( + &self, + config: &JoshConfig, + josh_url: &str, + branch: &str, + ) -> anyhow::Result<()> { + run_command_at( + &["git", "fetch", josh_url, branch], + &std::env::current_dir().unwrap(), + self.verbose, + )?; + let head = if let Some(subtree_filter) = &config.subtree_filter { + let josh_filter = get_josh_filter(self.verbose)?; + josh_filter.run( + &[subtree_filter, "HEAD"], + &std::env::current_dir().unwrap(), + self.verbose, + )?; + run_command(&["git", "rev-parse", "FILTERED_HEAD"], self.verbose) + .context("failed to get FILTERED_HEAD")? + } else { + get_current_head_sha(self.verbose)? + }; + let fetch_head = run_command(&["git", "rev-parse", "FETCH_HEAD"], self.verbose)?; + if head != fetch_head { + return Err(anyhow::anyhow!( + "Josh created a non-roundtrip push! Do NOT merge this into rustc!\n\ + Expected {head}, got {fetch_head}." + )); + } + println!( + "Confirmed that the push round-trips back to {} properly. Please create a rustc PR.", + self.context.config.repo + ); + Ok(()) + } +} + +fn get_josh_filter(verbose: bool) -> anyhow::Result { + println!("Updating/installing josh-filter binary..."); + match try_install_josh_filter(verbose) { + Some(filter) => Ok(filter), + None => Err(anyhow::anyhow!("Could not install josh-filter")), + } } /// Find a rustc repo we can do our push preparation in. diff --git a/src/utils.rs b/src/utils.rs index 87204e8..9c3a6ef 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,5 @@ use anyhow::Context; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::Command; /// Run command and return its stdout. @@ -36,6 +36,26 @@ fn run_command_inner<'a, Args: AsRef<[&'a str]>>( cmd.current_dir(workdir); cmd.args(&args[1..]); + execute_command(cmd, capture, verbose) +} + +pub fn run_command_by_path<'a, Args: AsRef<[&'a str]>>( + cmd: &PathBuf, + args: Args, + workdir: &Path, + capture: bool, + verbose: bool, +) -> anyhow::Result { + let args = args.as_ref(); + + let mut cmd = Command::new(cmd); + cmd.current_dir(workdir); + cmd.args(args); + + execute_command(cmd, capture, verbose) +} + +fn execute_command(mut cmd: Command, capture: bool, verbose: bool) -> anyhow::Result { if verbose { eprintln!("+ {cmd:?}"); } @@ -105,3 +125,8 @@ pub fn read_line() -> String { .expect("cannot read line from stdin"); line.trim().to_string() } + +pub fn is_null_sha(s: &str) -> bool { + let s = s.trim(); + !s.is_empty() && s.chars().all(|c| c == '0') +}