diff --git a/Cargo.lock b/Cargo.lock index 25c210b..786e5e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.19" @@ -267,6 +276,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "rustc-josh-sync" version = "0.1.0" @@ -274,6 +312,7 @@ dependencies = [ "anyhow", "clap", "directories", + "regex", "serde", "toml", "urlencoding", diff --git a/Cargo.toml b/Cargo.toml index 7ad5c90..a81d3fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ toml = "0.8" serde = { version = "1", features = ["derive"] } urlencoding = "2" which = "8" +regex = "1.12.3" [profile.release] debug = "line-tables-only" diff --git a/src/config.rs b/src/config.rs index 748e1fe..1688a3b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -42,11 +42,15 @@ impl JoshConfig { } pub fn construct_josh_filter(&self) -> String { - match (&self.path, &self.filter) { + 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<()> { @@ -74,3 +78,119 @@ 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 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 20e2d71..a6f8e4c 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 = "r24.10.04"; +const JOSH_VERSION: &str = "r26.05.08"; pub struct JoshProxy { path: PathBuf, @@ -120,7 +120,7 @@ pub fn try_install_josh_filter(verbose: bool) -> Option { "https://github.com/josh-project/josh", "--tag", JOSH_VERSION, - "josh-filter", + "josh-cli", ], verbose, ) @@ -137,6 +137,7 @@ pub struct RunningJoshProxy { impl RunningJoshProxy { pub fn git_url(&self, repo: &str, commit: Option<&str>, filter: &str) -> String { let commit = commit.map(|c| format!("@{c}")).unwrap_or_default(); + let filter = urlencoding::encode(filter); format!( "http://localhost:{}/{repo}.git{commit}{filter}.git", self.port