From c99569ff95358bb95b86c598aea95688293066ee Mon Sep 17 00:00:00 2001 From: Christian Schilling Date: Sat, 9 May 2026 15:42:33 +0200 Subject: [PATCH] Upgrade to Josh r26.05.08 This is a first upgrade step that sets backward compatibility options. The adaption to new behaviour can be done in a later step. Since setting the options for the filter requires quotes in the filter, this also changes URL construction to always encode special characters using urlencode. This is safer anyway. Change: upgrade-josh --- Cargo.lock | 39 ++++++++++++++++ Cargo.toml | 1 + src/config.rs | 124 +++++++++++++++++++++++++++++++++++++++++++++++++- src/josh.rs | 5 +- 4 files changed, 165 insertions(+), 4 deletions(-) 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