diff --git a/README.md b/README.md index 4a3c111..94ad9ec 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ If you need to specify a more complex Josh `filter`, use `filter` field in the c The `init` command will also create an empty `rust-version` file (if it doesn't already exist) that stores the last upstream `rustc` SHA that was synced in the subtree. +The [`josh-sync.example.toml`](josh-sync.example.toml) file contains all the things that can be configured. + ## Performing pull A pull operation fetches changes to the subtree subdirectory that were performed in `rust-lang/rust` and merges them into the subtree repository. After performing a pull, a pull request is sent against the *subtree repository*. We *pull from rustc*. @@ -25,7 +27,10 @@ A pull operation fetches changes to the subtree subdirectory that were performed 2) Create a new branch that will be used for the subtree PR, e.g. `pull` 3) Run `rustc-josh-sync pull` 4) Send a PR to the subtree repository - - Note that `rustc-josh-sync` can do this for you if you have the [gh](https://cli.github.com/) CLI tool installed. + +- Note that `rustc-josh-sync` can do this for you if you have the [gh](https://cli.github.com/) CLI tool installed. + +You can also configure a set of postprocessing operations to be performed after a successful pull using the `post-pull` configuration. ## Performing push @@ -33,7 +38,9 @@ A push operation takes changes performed in the subtree repository and merges th 1) Checkout the latest default branch of the subtree 2) Run `rustc-josh-sync push ` - - The branch with the push contents will be created in `https://github.com//rust` fork, in the `` branch. + +- The branch with the push contents will be created in `https://github.com//rust` fork, in the `` branch. + 3) Send a PR to [rust-lang/rust] ## Automating pulls on CI diff --git a/josh-sync.example.toml b/josh-sync.example.toml index a0a0dad..998005b 100644 --- a/josh-sync.example.toml +++ b/josh-sync.example.toml @@ -8,3 +8,10 @@ path = "library/stdarch" # Optionally, you can specify the complete Josh filter for complex cases # Note that this option is mutually exclusive with `path` #filter = ... + +# Optionally, you can specify a set of commands executed after a successful pull. +# If the executed command changes the local git state (performs some modifications to files that +# were already tracked), then a new commit with the given message will be created. +#[[post-pull]] +#cmd = ["cargo", "fmt"] +#commit-message = "reformat" diff --git a/src/bin/rustc_josh_sync.rs b/src/bin/rustc_josh_sync.rs index ecaa36f..72b62dd 100644 --- a/src/bin/rustc_josh_sync.rs +++ b/src/bin/rustc_josh_sync.rs @@ -84,6 +84,7 @@ fn main() -> anyhow::Result<()> { repo: "".to_string(), path: Some("".to_string()), filter: None, + post_pull: vec![], }; config .write(Path::new(DEFAULT_CONFIG_PATH)) diff --git a/src/config.rs b/src/config.rs index 6acb209..ccbd9bd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,6 +2,7 @@ use anyhow::Context; use std::path::Path; #[derive(serde::Serialize, serde::Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] pub struct JoshConfig { #[serde(default = "default_org")] pub org: String, @@ -12,6 +13,24 @@ pub struct JoshConfig { /// Optional filter specification for Josh. /// It cannot be used together with `path`. pub filter: Option, + /// Operation(s) that should be performed after a pull. + /// 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, +} + +/// Execute an operation after a pull, and if something changes in the local git state, +/// perform a commit. +#[derive(serde::Serialize, serde::Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct PostPullOperation { + /// Execute a command with these arguments + /// At least one argument has to be present. + /// You can run e.g. bash if you want to do more complicated stuff. + pub cmd: Vec, + /// If the git state has changed after `cmd`, add all changes to the index (`git add -u`) + /// and create a commit with the following commit message. + pub commit_message: String, } impl JoshConfig { diff --git a/src/sync.rs b/src/sync.rs index 7270312..92adcac 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,4 +1,5 @@ use crate::SyncContext; +use crate::config::PostPullOperation; use crate::josh::JoshProxy; use crate::utils::{ensure_clean_git_state, prompt}; use crate::utils::{get_current_head_sha, run_command_at}; @@ -65,7 +66,7 @@ impl GitSync { .to_owned() }; - ensure_clean_git_state(self.verbose); + ensure_clean_git_state(self.verbose)?; // Make sure josh is running. let josh = self @@ -220,17 +221,21 @@ After you fix the conflicts, `git add` the changes and run `git merge --continue // But it can be more tricky - we can have only empty merge/rollup merge commits from // rustc, so a merge was created, but the in-tree diff can still be empty. // In that case we also bail. - // `git diff --exit-code` "succeeds" if the diff is empty. - if run_command( - &["git", "diff", "--exit-code", &sha_pre_merge], - self.verbose, - ) - .is_ok() - { + if self.has_empty_diff(&sha_pre_merge) { eprintln!("Only empty changes were pulled. Rolling back the preparation commit."); return Err(RustcPullError::NothingToPull); } + println!("Pull finished! Current HEAD is {current_sha}"); + + if !self.context.config.post_pull.is_empty() { + println!("Running post-pull operation(s)"); + + for op in &self.context.config.post_pull { + self.run_post_pull_op(&op)?; + } + } + git_reset.disarm(); // Check that the number of roots did not change. @@ -241,14 +246,13 @@ After you fix the conflicts, `git add` the changes and run `git merge --continue .into()); } - println!("Pull finished! Current HEAD is {current_sha}"); Ok(PullResult { merge_commit_message: merge_message, }) } pub fn rustc_push(&self, username: &str, branch: &str) -> anyhow::Result<()> { - ensure_clean_git_state(self.verbose); + ensure_clean_git_state(self.verbose)?; let base_upstream_sha = self.context.last_upstream_sha.clone().unwrap_or_default(); @@ -341,6 +345,27 @@ After you fix the conflicts, `git add` the changes and run `git merge --continue Ok(()) } + + fn has_empty_diff(&self, baseline_sha: &str) -> bool { + // `git diff --exit-code` "succeeds" if the diff is empty. + run_command(&["git", "diff", "--exit-code", baseline_sha], self.verbose).is_ok() + } + + fn run_post_pull_op(&self, op: &PostPullOperation) -> anyhow::Result<()> { + let head = get_current_head_sha(self.verbose)?; + run_command(op.cmd.iter().map(|s| s.as_str()).collect::>(), true)?; + if !self.has_empty_diff(&head) { + println!( + "`{}` changed something, committing with message `{}`", + op.cmd.join(" "), + op.commit_message + ); + run_command(["git", "add", "-u"], self.verbose)?; + run_command(["git", "commit", "-m", &op.commit_message], self.verbose)?; + } + + Ok(()) + } } /// Find a rustc repo we can do our push preparation in. diff --git a/src/utils.rs b/src/utils.rs index 0e6bf55..87204e8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -69,13 +69,17 @@ fn run_command_inner<'a, Args: AsRef<[&'a str]>>( } /// Fail if there are files that need to be checked in. -pub fn ensure_clean_git_state(verbose: bool) { +pub fn ensure_clean_git_state(verbose: bool) -> anyhow::Result<()> { let read = run_command( ["git", "status", "--untracked-files=no", "--porcelain"], verbose, ) .expect("cannot figure out if git state is clean"); - assert!(read.is_empty(), "working directory must be clean"); + if !read.is_empty() { + Err(anyhow::anyhow!("working directory must be clean")) + } else { + Ok(()) + } } pub fn get_current_head_sha(verbose: bool) -> anyhow::Result {