diff --git a/josh-sync.example.toml b/josh-sync.example.toml index ece01db..6df984b 100644 --- a/josh-sync.example.toml +++ b/josh-sync.example.toml @@ -9,12 +9,12 @@ path = "library/stdarch" # 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" +# Optionally, you can specify the filter version, which tells josh-sync which postprocessing to +# apply to the specified filter. +# If not specified, it defaults to version 1. +# Version 1 - default "legacy" behavior of Josh, which preserves empty merge commits +# Version 2 - newer behavior that skips empty merge commits +#filter-version = 1 # Optionally, you can specify a subtree filter. # This will be applied to the local `HEAD` during the round-trip check. @@ -28,3 +28,10 @@ path = "library/stdarch" # E.g., if the `filter` is ":/compiler/rustc_public:prefix=rustc_public", # the `subtree-filter` should be: #subtree-filter = ":/rustc_public:prefix=rustc_public" + +# 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 edb72d2..3a64072 100644 --- a/src/bin/rustc_josh_sync.rs +++ b/src/bin/rustc_josh_sync.rs @@ -3,7 +3,7 @@ use clap::Parser; use rustc_josh_sync::SyncContext; use rustc_josh_sync::config::{JoshConfig, load_config}; use rustc_josh_sync::josh::{JoshProxy, try_install_josh_proxy}; -use rustc_josh_sync::sync::{DEFAULT_UPSTREAM_REPO, GitSync, RustcPullError}; +use rustc_josh_sync::sync::{DEFAULT_UPSTREAM_REPO, FilterVersion, GitSync, RustcPullError}; use rustc_josh_sync::utils::{get_current_head_sha, prompt}; use std::path::{Path, PathBuf}; @@ -90,6 +90,7 @@ fn main() -> anyhow::Result<()> { filter: None, post_pull: vec![], subtree_filter: None, + filter_version: FilterVersion::latest(), }; config .write(Path::new(DEFAULT_CONFIG_PATH)) diff --git a/src/config.rs b/src/config.rs index 02833f0..7630c17 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use crate::sync::FilterVersion; use anyhow::Context; use std::path::Path; @@ -20,6 +21,70 @@ pub struct JoshConfig { /// Optional subtree filter applied to the local `HEAD` during round-trip check. #[serde(default, skip_serializing_if = "Option::is_none")] pub subtree_filter: Option, + /// Optional filter version that determines which post-processing will be applied to the + /// specified filter. + /// + /// This exists for backwards compatibility with repositories using an older filter syntax. + #[serde( + default = "default_filter_version", + skip_serializing_if = "skip_serializing_filter_version", + with = "filter_version" + )] + pub filter_version: FilterVersion, +} + +impl JoshConfig { + pub fn full_repo_name(&self) -> String { + format!("{}/{}", self.org, self.repo) + } + + pub fn write(&self, path: &Path) -> anyhow::Result<()> { + let config = toml::to_string_pretty(self).context("cannot serialize config")?; + std::fs::write(path, config).context("cannot write config")?; + Ok(()) + } +} + +mod filter_version { + use crate::sync::FilterVersion; + use serde::de::{Error, Unexpected}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize( + version: &FilterVersion, + serializer: S, + ) -> Result { + let num: u32 = match version { + FilterVersion::Version1 => 1, + FilterVersion::Version2 => 2, + }; + num.serialize(serializer) + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result { + let num = u32::deserialize(deserializer)?; + match num { + 1 => Ok(FilterVersion::Version1), + 2 => Ok(FilterVersion::Version2), + v => Err(D::Error::invalid_value( + Unexpected::Unsigned(v as u64), + &"1 or 2", + )), + } + } +} + +fn default_filter_version() -> FilterVersion { + FilterVersion::Version1 +} + +fn skip_serializing_filter_version(version: &FilterVersion) -> bool { + match version { + FilterVersion::Version1 => true, + FilterVersion::Version2 => false, + } } /// Execute an operation after a pull, and if something changes in the local git state, @@ -36,30 +101,6 @@ pub struct PostPullOperation { pub commit_message: String, } -impl JoshConfig { - pub fn full_repo_name(&self) -> String { - format!("{}/{}", self.org, self.repo) - } - - pub fn construct_josh_filter(&self) -> String { - let filter = match (&self.path, &self.filter) { - (Some(path), None) => format!(":/{path}"), - (None, Some(filter)) => filter.clone(), - _ => unreachable!("Config contains both path and a filter"), - }; - - let filter = convert_rev_syntax(&filter); - let filter = wrap_compat(&filter); - filter - } - - pub fn write(&self, path: &Path) -> anyhow::Result<()> { - let config = toml::to_string_pretty(self).context("cannot serialize config")?; - std::fs::write(path, config).context("cannot write config")?; - Ok(()) - } -} - fn default_org() -> String { String::from("rust-lang") } @@ -78,138 +119,3 @@ pub fn load_config(path: &Path) -> anyhow::Result { Ok(config) } - -/// Converts filters from old `:rev(sha:filter)` syntax to new -/// `:rev(<=sha:filter)` syntax. Null SHAs (40 zeros) become `_`. -/// Only touches SHAs inside `:rev(...)` blocks. -fn convert_rev_syntax(input: &str) -> String { - let rev_block = regex::Regex::new(r":rev\([^)]*\)").unwrap(); - let entry = regex::Regex::new( - r"(?x) - ([,(]) # delimiter before entry - (0{40}|[0-9a-f]{40}) # full SHA - : # colon separator - ", - ) - .unwrap(); - - rev_block - .replace_all(input, |block: ®ex::Captures| { - entry - .replace_all(&block[0], |caps: ®ex::Captures| { - let delim = &caps[1]; - let sha = &caps[2]; - if sha.chars().all(|c| c == '0') { - format!("{delim}_:") - } else { - format!("{delim}<={sha}:") - } - }) - .into_owned() - }) - .into_owned() -} - -/// Wraps a filter with the backwards compatibility meta options for -/// trivial merge preservation and CRLF normalization in gpgsig headers. -/// -/// `:your/filter` becomes -/// `:~(history="keep-trivial-merges",gpgsig="norm-lf")[:your/filter]` -fn wrap_compat(filter: &str) -> String { - format!(":~(history=\"keep-trivial-merges\",gpgsig=\"norm-lf\")[{filter}]") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn no_rev_block_unchanged() { - assert_eq!(convert_rev_syntax(":/some/path"), ":/some/path"); - } - - #[test] - fn single_sha_gets_prefix() { - assert_eq!( - convert_rev_syntax(":rev(3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/some/path)"), - ":rev(<=3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/some/path)", - ); - } - - #[test] - fn null_sha_becomes_underscore() { - assert_eq!( - convert_rev_syntax(":rev(0000000000000000000000000000000000000000:/some/path)"), - ":rev(_:/some/path)", - ); - } - - #[test] - fn multiple_entries_in_rev_block() { - assert_eq!( - convert_rev_syntax( - ":rev(3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/p1,\ - e4c7a2d8f1b3e5a9d6c0f2b4a7e1d3c5f8a0b6e9:/p2,\ - 0000000000000000000000000000000000000000:/p3)" - ), - ":rev(<=3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/p1,\ - <=e4c7a2d8f1b3e5a9d6c0f2b4a7e1d3c5f8a0b6e9:/p2,\ - _:/p3)", - ); - } - - #[test] - fn already_converted_syntax_unchanged() { - assert_eq!( - convert_rev_syntax(":rev(<=3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/some/path)"), - ":rev(<=3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/some/path)", - ); - } - - #[test] - fn underscore_syntax_unchanged() { - assert_eq!( - convert_rev_syntax(":rev(_:/some/path)"), - ":rev(_:/some/path)", - ); - } - - #[test] - fn sha_outside_rev_block_unchanged() { - assert_eq!( - convert_rev_syntax("3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/some/path"), - "3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/some/path", - ); - } - - #[test] - fn wrap_compat_simple_filter() { - assert_eq!( - wrap_compat(":/some/path"), - ":~(history=\"keep-trivial-merges\",gpgsig=\"norm-lf\")[:/some/path]", - ); - } - - #[test] - fn wrap_compat_rev_filter() { - assert_eq!( - wrap_compat( - ":rev(75dd959a3a40eb5b4574f8d2e23aa6efbeb33573:prefix=src/tools/miri):/src/tools/miri" - ), - ":~(history=\"keep-trivial-merges\",gpgsig=\"norm-lf\")\ - [:rev(75dd959a3a40eb5b4574f8d2e23aa6efbeb33573:prefix=src/tools/miri):/src/tools/miri]", - ); - } - - #[test] - fn multiple_rev_blocks() { - assert_eq!( - convert_rev_syntax( - ":rev(3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/p1)\ - :rev(e4c7a2d8f1b3e5a9d6c0f2b4a7e1d3c5f8a0b6e9:/p2)" - ), - ":rev(<=3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/p1)\ - :rev(<=e4c7a2d8f1b3e5a9d6c0f2b4a7e1d3c5f8a0b6e9:/p2)", - ); - } -} diff --git a/src/josh.rs b/src/josh.rs index fe58e37..fb0a65b 100644 --- a/src/josh.rs +++ b/src/josh.rs @@ -8,7 +8,7 @@ use std::time::Duration; const JOSH_PORT: u16 = 42042; /// Version of `josh-proxy` that should be downloaded for the user. -const JOSH_VERSION: &str = "r26.06.11"; +const JOSH_VERSION: &str = "r26.07.19"; pub struct JoshProxy { path: PathBuf, diff --git a/src/sync.rs b/src/sync.rs index e31a164..9c3339a 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -22,6 +22,20 @@ impl From for RustcPullError { } } +#[derive(Copy, Clone)] +pub enum FilterVersion { + /// Keep empty merge commits. + Version1, + /// Skip empty merge commits. + Version2, +} + +impl FilterVersion { + pub fn latest() -> Self { + Self::Version2 + } +} + pub struct PullResult { pub merge_commit_message: String, } @@ -77,7 +91,7 @@ impl GitSync { let josh_url = josh.git_url( &upstream_repo, Some(&upstream_sha), - &self.context.config.construct_josh_filter(), + &construct_josh_filter(&self.context.config), ); let orig_head = get_current_head_sha(self.verbose)?; @@ -268,7 +282,7 @@ After you fix the conflicts, `git add` the changes and run `git merge --continue let josh_url = josh.git_url( &format!("{username}/rust"), None, - &self.context.config.construct_josh_filter(), + &construct_josh_filter(&self.context.config), ); let user_upstream_url = format!("https://github.com/{username}/rust"); @@ -475,3 +489,158 @@ impl Drop for GitResetOnDrop { } } } + +fn construct_josh_filter(config: &JoshConfig) -> String { + let filter = match (&config.path, &config.filter) { + (Some(path), None) => format!(":/{path}"), + (None, Some(filter)) => filter.clone(), + _ => panic!("Config contains both path and a filter"), + }; + match config.filter_version { + // Keep backwards compatibility with repositories that started with a legacy version of + // Josh. + FilterVersion::Version1 => { + // Convert old :rev syntax + let filter = convert_rev_syntax(&filter); + // Keep empty merges + wrap_compat(&filter) + } + // Use the current default behavior of Josh. + FilterVersion::Version2 => filter, + } +} + +/// Converts filters from old `:rev(sha:filter)` syntax to new +/// `:rev(<=sha:filter)` syntax. Null SHAs (40 zeros) become `_`. +/// Only touches SHAs inside `:rev(...)` blocks. +fn convert_rev_syntax(input: &str) -> String { + let rev_block = regex::Regex::new(r":rev\([^)]*\)").unwrap(); + let entry = regex::Regex::new( + r"(?x) + ([,(]) # delimiter before entry + (0{40}|[0-9a-f]{40}) # full SHA + : # colon separator + ", + ) + .unwrap(); + + rev_block + .replace_all(input, |block: ®ex::Captures| { + entry + .replace_all(&block[0], |caps: ®ex::Captures| { + let delim = &caps[1]; + let sha = &caps[2]; + if sha.chars().all(|c| c == '0') { + format!("{delim}_:") + } else { + format!("{delim}<={sha}:") + } + }) + .into_owned() + }) + .into_owned() +} + +/// Wraps a filter with the backwards compatibility meta options for +/// trivial merge preservation and CRLF normalization in gpgsig headers. +/// +/// `:your/filter` becomes +/// `:~(history="keep-trivial-merges",gpgsig="norm-lf")[:your/filter]` +fn wrap_compat(filter: &str) -> String { + format!(":~(history=\"keep-trivial-merges\",gpgsig=\"norm-lf\")[{filter}]") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn no_rev_block_unchanged() { + assert_eq!(convert_rev_syntax(":/some/path"), ":/some/path"); + } + + #[test] + fn single_sha_gets_prefix() { + assert_eq!( + convert_rev_syntax(":rev(3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/some/path)"), + ":rev(<=3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/some/path)", + ); + } + + #[test] + fn null_sha_becomes_underscore() { + assert_eq!( + convert_rev_syntax(":rev(0000000000000000000000000000000000000000:/some/path)"), + ":rev(_:/some/path)", + ); + } + + #[test] + fn multiple_entries_in_rev_block() { + assert_eq!( + convert_rev_syntax( + ":rev(3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/p1,\ + e4c7a2d8f1b3e5a9d6c0f2b4a7e1d3c5f8a0b6e9:/p2,\ + 0000000000000000000000000000000000000000:/p3)" + ), + ":rev(<=3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/p1,\ + <=e4c7a2d8f1b3e5a9d6c0f2b4a7e1d3c5f8a0b6e9:/p2,\ + _:/p3)", + ); + } + + #[test] + fn already_converted_syntax_unchanged() { + assert_eq!( + convert_rev_syntax(":rev(<=3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/some/path)"), + ":rev(<=3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/some/path)", + ); + } + + #[test] + fn underscore_syntax_unchanged() { + assert_eq!( + convert_rev_syntax(":rev(_:/some/path)"), + ":rev(_:/some/path)", + ); + } + + #[test] + fn sha_outside_rev_block_unchanged() { + assert_eq!( + convert_rev_syntax("3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/some/path"), + "3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/some/path", + ); + } + + #[test] + fn wrap_compat_simple_filter() { + assert_eq!( + wrap_compat(":/some/path"), + ":~(history=\"keep-trivial-merges\",gpgsig=\"norm-lf\")[:/some/path]", + ); + } + + #[test] + fn wrap_compat_rev_filter() { + assert_eq!( + wrap_compat( + ":rev(75dd959a3a40eb5b4574f8d2e23aa6efbeb33573:prefix=src/tools/miri):/src/tools/miri" + ), + ":~(history=\"keep-trivial-merges\",gpgsig=\"norm-lf\")\ + [:rev(75dd959a3a40eb5b4574f8d2e23aa6efbeb33573:prefix=src/tools/miri):/src/tools/miri]", + ); + } + + #[test] + fn multiple_rev_blocks() { + assert_eq!( + convert_rev_syntax( + ":rev(3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/p1)\ + :rev(e4c7a2d8f1b3e5a9d6c0f2b4a7e1d3c5f8a0b6e9:/p2)" + ), + ":rev(<=3a1f5e2b9c8d4e7f6a0b1c2d3e4f5a6b7c8d9e0f:/p1)\ + :rev(<=e4c7a2d8f1b3e5a9d6c0f2b4a7e1d3c5f8a0b6e9:/p2)", + ); + } +}