Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package riff for openSUSE:Factory checked in at 2026-11-09 16:07:54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/riff (Old) and /work/SRC/openSUSE:Factory/.riff.new.3759290 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "riff" Mon Nov 9 16:07:54 2026 rev:30 rq:1396297 version:3.7.0 Changes: -------- --- /work/SRC/openSUSE:Factory/riff/riff.changes 2026-08-17 17:02:51.284219112 +0200 +++ /work/SRC/openSUSE:Factory/.riff.new.3759290/riff.changes 2026-11-09 16:08:55.210563845 +0100 @@ -1,0 +2,10 @@ +Mon Nov 9 09:58:57 UTC 2026 - Michael Vetter <[email protected]> + +- Update to 3.7.0: + Handle jj diff --tool riff better: + * jj's left/ and right/ path prefixes are no longer highlighted as changes in + j file headers + * Hyperlinks now point to your real working copy files, not to jj's + temporary diff directory + +------------------------------------------------------------------- Old: ---- riff-3.6.2.tar.zst New: ---- riff-3.7.0.tar.zst ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ riff.spec ++++++ --- /var/tmp/diff_new_pack.5GADN7/_old 2026-11-09 16:08:56.381612899 +0100 +++ /var/tmp/diff_new_pack.5GADN7/_new 2026-11-09 16:08:56.382612941 +0100 @@ -17,7 +17,7 @@ Name: riff -Version: 3.6.2 +Version: 3.7.0 Release: 0 Summary: A diff filter highlighting which line parts have changed License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.5GADN7/_old 2026-11-09 16:08:56.415614323 +0100 +++ /var/tmp/diff_new_pack.5GADN7/_new 2026-11-09 16:08:56.419614491 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/walles/riff.git</param> <param name="versionformat">@PARENT_TAG@</param> <param name="scm">git</param> - <param name="revision">3.6.2</param> + <param name="revision">3.7.0</param> <param name="match-tag">*</param> <param name="versionrewrite-pattern">v(\d+\.\d+\.\d+)</param> <param name="versionrewrite-replacement">\1</param> ++++++ riff-3.6.2.tar.zst -> riff-3.7.0.tar.zst ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.2/Cargo.lock new/riff-3.7.0/Cargo.lock --- old/riff-3.6.2/Cargo.lock 2026-08-15 23:27:15.000000000 +0200 +++ new/riff-3.7.0/Cargo.lock 2026-11-06 20:32:11.000000000 +0100 @@ -557,7 +557,7 @@ [[package]] name = "riffdiff" -version = "3.6.2" +version = "3.7.0" dependencies = [ "backtrace", "base64", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.2/Cargo.toml new/riff-3.7.0/Cargo.toml --- old/riff-3.6.2/Cargo.toml 2026-08-15 23:27:15.000000000 +0200 +++ new/riff-3.7.0/Cargo.toml 2026-11-06 20:32:11.000000000 +0100 @@ -2,7 +2,7 @@ [package] name = "riffdiff" # Actually "riff", but that was already taken on crates.io -version = "3.6.2" +version = "3.7.0" authors = ["Johan Walles <[email protected]>"] edition = "2018" repository = "https://github.com/walles/riff/" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.2/ci.sh new/riff-3.7.0/ci.sh --- old/riff-3.6.2/ci.sh 2026-08-15 23:27:15.000000000 +0200 +++ new/riff-3.7.0/ci.sh 2026-11-06 20:32:11.000000000 +0100 @@ -16,7 +16,7 @@ rustup component add clippy rustfmt # Settings are at the top of main.rs -cargo clippy +cargo clippy --all-targets # Copied from here: # <https://docs.travis-ci.com/user/languages/rust/#default-build-script> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.2/src/file_highlighter.rs new/riff-3.7.0/src/file_highlighter.rs --- old/riff-3.6.2/src/file_highlighter.rs 2026-08-15 23:27:15.000000000 +0200 +++ new/riff-3.7.0/src/file_highlighter.rs 2026-11-06 20:32:11.000000000 +0100 @@ -14,6 +14,13 @@ use crate::hunk_highlighter::HunkLinesHighlighter; use crate::refiner::Formatter; +/// Old / new path prefixes we lowlight in file headers, and strip when looking +/// for files to hyperlink. +/// +/// - `a` / `b` are from `git diff` +/// - `left` / `right` are from `jj diff --tool riff` +const PATH_PREFIX_PAIRS: &[(&str, &str)] = &[("a", "b"), ("left", "right")]; + pub(crate) struct FileHighlighter { /// May or may not end with one or more tabs + a timestamp string. old_name: String, @@ -242,30 +249,57 @@ } } +/// Directories to resolve relative paths against, in order of preference. +/// +/// `$PWD` goes first because `jj diff --tool riff` runs us with cwd in a +/// temporary directory that jj deletes when we exit, while leaving `$PWD` +/// pointing at the user's directory. Linking into the temporary directory +/// would give dead links. +/// +/// The cwd is the fallback for when `$PWD` is unset, or has gone stale for some +/// other reason. `cargo test` from a subdirectory does that for example. +fn base_dirs() -> Vec<std::path::PathBuf> { + let mut base_dirs = Vec::new(); + + if let Some(pwd) = std::env::var_os("PWD") { + let pwd = std::path::PathBuf::from(pwd); + if pwd.is_absolute() { + base_dirs.push(pwd); + } + } + + // Empty means relative to the cwd, even if we can't tell what it is + base_dirs.push(std::env::current_dir().unwrap_or_default()); + + return base_dirs; +} + /// This function will try to find the file, either by the passed-in name or by -/// stripping a/ or b/ prefixes. +/// stripping any of the `PATH_PREFIX_PAIRS` prefixes. +/// +/// Relative paths are looked up in each of the `base_dirs()`, trying all +/// candidates in one directory before moving on to the next. fn hyperlink_filename(filename: &str) -> Option<url::Url> { let mut raw_candidates: Vec<&str> = Vec::new(); raw_candidates.push(filename); - if let Some(no_a_prefix) = filename.strip_prefix("a/") { - raw_candidates.push(no_a_prefix); - } - if let Some(no_b_prefix) = filename.strip_prefix("b/") { - raw_candidates.push(no_b_prefix); - } - - for candidate in raw_candidates { - if candidate == "/dev/null" { - continue; + for prefix in PATH_PREFIX_PAIRS.iter().flat_map(|(old, new)| [old, new]) { + if let Some(without_prefix) = filename + .strip_prefix(prefix) + .and_then(|rest| rest.strip_prefix('/')) + { + raw_candidates.push(without_prefix); } + } - let mut path = std::path::PathBuf::from(candidate); - if !path.is_absolute() { - if let Ok(cwd) = std::env::current_dir() { - path = cwd.join(&path); - } - } + let paths = base_dirs().into_iter().flat_map(|base_dir| { + raw_candidates + .iter() + .filter(|candidate| **candidate != "/dev/null") + // Joining an absolute path just returns that absolute path + .map(move |candidate| base_dir.join(candidate)) + }); + for path in paths { if !path.exists() { continue; } @@ -379,7 +413,7 @@ /// Splits a row into (prefix, just_path, full_path, just_filename, timestamp) slices. /// -/// Let's say we have a row like this and `look_for_git_prefixes` is true: +/// Let's say we have a row like this and `look_for_path_prefixes` is true: /// ``` /// a/doc/c.txt\t2023-12-15 15:43:29 /// ``` @@ -390,13 +424,14 @@ /// - `just_filename`: `c.txt` /// - `time_space`: `\t` /// - `timestamp`: `2023-12-15 15:43:29` -fn split_row<'a>(look_for_git_prefixes: bool, row: &'a mut [StyledToken]) -> SplitRow<'a> { - let path_start = if look_for_git_prefixes - && row.len() >= 2 - && row[0].token.len() == 1 // "a" or "b" - && (row[1].token == "/" || row[1].token == std::path::MAIN_SEPARATOR.to_string()) - { - // If we have a git prefix, the path starts after the first two tokens +fn split_row<'a>(look_for_path_prefixes: bool, row: &'a mut [StyledToken]) -> SplitRow<'a> { + let is_any_prefix = |tokens: &[StyledToken]| { + PATH_PREFIX_PAIRS + .iter() + .any(|(old, new)| starts_with_prefix(tokens, old) || starts_with_prefix(tokens, new)) + }; + let path_start = if look_for_path_prefixes && is_any_prefix(row) { + // If we have a path prefix, the path starts after the first two tokens 2 } else { // Otherwise, it starts at the first token @@ -443,24 +478,26 @@ }; } -fn have_git_prefixes(old_tokens: &[StyledToken], new_tokens: &[StyledToken]) -> bool { - // Both "a/..." and "/dev/null" count as having a git prefix. +fn is_separator(token: &StyledToken) -> bool { + return token.token == "/" || token.token == std::path::MAIN_SEPARATOR.to_string(); +} + +/// True if `tokens` start with `prefix` followed by a path separator +fn starts_with_prefix(tokens: &[StyledToken], prefix: &str) -> bool { + return tokens.len() >= 2 && tokens[0].token == prefix && is_separator(&tokens[1]); +} - let old_has_git_prefix = old_tokens.len() >= 2 - && old_tokens[0].token == "a" - && (old_tokens[1].token == "/" - || old_tokens[1].token == std::path::MAIN_SEPARATOR.to_string()); - let old_is_absolute = !old_tokens.is_empty() && old_tokens[0].token == "/" - || old_tokens[0].token == std::path::MAIN_SEPARATOR.to_string(); - - let new_has_git_prefix = new_tokens.len() >= 2 - && new_tokens[0].token == "b" - && (new_tokens[1].token == "/" - || new_tokens[1].token == std::path::MAIN_SEPARATOR.to_string()); - let new_is_absolute = !new_tokens.is_empty() && new_tokens[0].token == "/" - || new_tokens[0].token == std::path::MAIN_SEPARATOR.to_string(); +/// True if both paths have matching prefixes from `PATH_PREFIX_PAIRS`, like +/// "a/..." and "b/...". An absolute path, like "/dev/null", matches any +/// prefix. +fn have_path_prefixes(old_tokens: &[StyledToken], new_tokens: &[StyledToken]) -> bool { + let old_is_absolute = old_tokens.first().is_some_and(is_separator); + let new_is_absolute = new_tokens.first().is_some_and(is_separator); - return (old_has_git_prefix || old_is_absolute) && (new_has_git_prefix || new_is_absolute); + return PATH_PREFIX_PAIRS.iter().any(|(old_prefix, new_prefix)| { + (old_is_absolute || starts_with_prefix(old_tokens, old_prefix)) + && (new_is_absolute || starts_with_prefix(new_tokens, new_prefix)) + }); } pub(crate) fn decorate_paths( @@ -468,10 +505,10 @@ new_tokens: &mut [StyledToken], fragment_line: Option<usize>, ) { - let look_for_git_prefixes = have_git_prefixes(old_tokens, new_tokens); + let look_for_path_prefixes = have_path_prefixes(old_tokens, new_tokens); - let old_split = split_row(look_for_git_prefixes, old_tokens); - let new_split = split_row(look_for_git_prefixes, new_tokens); + let old_split = split_row(look_for_path_prefixes, old_tokens); + let new_split = split_row(look_for_path_prefixes, new_tokens); // Brighten file names old_split.just_filename.iter_mut().for_each(|token| { @@ -514,7 +551,7 @@ token.style = Style::Lowlighted; }); - // Lowlight git prefixes + // Lowlight path prefixes old_split.prefix.iter_mut().for_each(|token| { token.style = Style::Lowlighted; }); @@ -635,6 +672,35 @@ } #[test] + fn test_brighten_filename_left_right_prefixes() { + // jj uses left/ and right/ when running riff as its diff tool: + // https://github.com/walles/riff/issues/82 + let highlighted = highlight_header_lines("--- left/x/y/z.txt", "+++ right/x/y/z.txt"); + assert_eq!( + format!( + "\ + {BOLD}--- {NORMAL_INTENSITY}{FAINT}left/{NORMAL}x/y/{BOLD}z.txt{NORMAL}\n\ + {BOLD}+++ {NORMAL_INTENSITY}{FAINT}right/{NORMAL}x/y/{BOLD}z.txt{NORMAL}\n" + ), + highlighted + ); + } + + #[test] + fn test_mismatched_prefixes_are_highlighted() { + // a/ only goes with b/, and left/ only goes with right/ + let highlighted = highlight_header_lines("--- a/z.txt", "+++ right/z.txt"); + assert_eq!( + format!( + "\ + {BOLD}--- {INVERSE_VIDEO}{NORMAL_INTENSITY}{OLD}a{NORMAL}/{BOLD}z.txt{NORMAL}\n\ + {BOLD}+++ {INVERSE_VIDEO}{NORMAL_INTENSITY}{GREEN}right{NORMAL}/{BOLD}z.txt{NORMAL}\n" + ), + highlighted + ); + } + + #[test] fn test_brighten_filename_without_path() { let highlighted = highlight_header_lines("--- z.txt", "+++ z.txt"); assert_eq!( @@ -767,6 +833,36 @@ } #[test] + fn test_hyperlink_filename_right_prefix() { + let url_opt = super::hyperlink_filename("right/README.md"); + + let path_from_url = url_opt + .expect("Expected Some(url::Url) for right/ prefixed path") + .to_file_path() + .expect("Should convert URL back to path"); + let expected = std::fs::canonicalize("README.md").expect("Canonicalize README.md"); + let actual = std::fs::canonicalize(&path_from_url).expect("Canonicalize URL path"); + assert_eq!(actual, expected, "Canonical paths must match"); + } + + #[test] + fn test_split_row_with_multi_character_prefix() { + let mut tokens: Vec<StyledToken> = ["left", "/", "doc", "/", "c.txt"] + .iter() + .map(|s| StyledToken::new(s.to_string(), Style::Context)) + .collect(); + let split = split_row(true, &mut tokens); + assert_eq!( + split.prefix.iter().map(|t| &t.token).collect::<Vec<_>>(), + ["left", "/"] + ); + assert_eq!( + split.just_path.iter().map(|t| &t.token).collect::<Vec<_>>(), + ["doc", "/"] + ); + } + + #[test] fn test_split_row_with_slash_separator() { let mut tokens: Vec<StyledToken> = ["doc", "/", "c.txt", "\t", "2023-12-15 15:43:29"] .iter() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.2/src/token_collector.rs new/riff-3.7.0/src/token_collector.rs --- old/riff-3.6.2/src/token_collector.rs 2026-08-15 23:27:15.000000000 +0200 +++ new/riff-3.7.0/src/token_collector.rs 2026-11-06 20:32:11.000000000 +0100 @@ -252,7 +252,7 @@ #[test] fn test_basic() { let rendered = render( - &*LINE_STYLE_NEW, + &LINE_STYLE_NEW, "+", &[ StyledToken { @@ -274,7 +274,7 @@ fn test_removed_trailing_whitespace() { // It shouldn't be highlighted, just added ones should let actual = render( - &*LINE_STYLE_OLD, + &LINE_STYLE_OLD, "-", &[StyledToken::new(" ".to_string(), Style::DiffPartMidlighted)], ); @@ -286,7 +286,7 @@ fn test_removed_nonleading_tab() { // It shouldn't be highlighted, just added ones should let actual = render( - &*LINE_STYLE_OLD, + &LINE_STYLE_OLD, "-", &[ StyledToken::new("x".to_string(), Style::DiffPartMidlighted), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.2/testdata/jj-left-right.diff new/riff-3.7.0/testdata/jj-left-right.diff --- old/riff-3.6.2/testdata/jj-left-right.diff 1970-01-01 01:00:00.000000000 +0100 +++ new/riff-3.7.0/testdata/jj-left-right.diff 2026-11-06 20:32:11.000000000 +0100 @@ -0,0 +1,7 @@ +diff -ur --show-c-function --new-file left/docs/greeting.txt right/docs/greeting.txt +--- left/docs/greeting.txt 2026-11-05 19:26:54.000000000 +0100 ++++ right/docs/greeting.txt 2026-11-05 19:26:54.000000000 +0100 +@@ -1,2 +1,2 @@ + Hello +-World ++there diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.2/testdata/jj-left-right.riff-output new/riff-3.7.0/testdata/jj-left-right.riff-output --- old/riff-3.6.2/testdata/jj-left-right.riff-output 1970-01-01 01:00:00.000000000 +0100 +++ new/riff-3.7.0/testdata/jj-left-right.riff-output 2026-11-06 20:32:11.000000000 +0100 @@ -0,0 +1,7 @@ +[2mdiff -ur --show-c-function --new-file left/docs/greeting.txt right/docs/greeting.txt[0m +[1m--- [22m[2mleft/[0mdocs/[1mgreeting.txt[0m [2m2026-11-05 19:26:54.000000000 +0100[0m +[1m+++ [22m[2mright/[0mdocs/[1mgreeting.txt[0m [2m2026-11-05 19:26:54.000000000 +0100[0m +[36m@@ -1,2 +1,2 @@[0m + Hello +[31m-[7mWorld[0m +[32m+[7mthere[0m diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.2/tests/pwd_hyperlinks.rs new/riff-3.7.0/tests/pwd_hyperlinks.rs --- old/riff-3.6.2/tests/pwd_hyperlinks.rs 1970-01-01 01:00:00.000000000 +0100 +++ new/riff-3.7.0/tests/pwd_hyperlinks.rs 2026-11-06 20:32:11.000000000 +0100 @@ -0,0 +1,94 @@ +// jj runs diff tools in a temporary directory containing left/ and right/ +// copies of the trees, deleting that directory when the tool exits. It leaves +// PWD pointing at the user's directory though, so that's where hyperlinks +// should point. + +#![cfg(unix)] + +use std::io::Write; +use std::path::Path; +use std::process::{Command, Stdio}; + +const OLD_C: &str = "int main() {\n 1;\n 2;\n 3;\n 4;\n 5;\n}\n"; +const NEW_C: &str = "int main() {\n 1;\n 2;\n 3;\n 4;\n 50;\n}\n"; + +fn file_url(path: &Path) -> String { + let canonical = std::fs::canonicalize(path).expect("Path should canonicalize"); + format!("file://{}", canonical.display()) +} + +fn write(path: &Path, contents: &str) { + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, contents).unwrap(); +} + +#[test] +fn test_jj_diff_tool_links_to_pwd_not_temp_dir() { + let repo = tempfile::tempdir().unwrap(); + write(&repo.path().join("m.c"), NEW_C); + + let jj_temp = tempfile::tempdir().unwrap(); + write(&jj_temp.path().join("left/m.c"), OLD_C); + write(&jj_temp.path().join("right/m.c"), NEW_C); + + // This is what jj does: cwd is the temp dir, PWD is the user's directory + let output = Command::new(env!("CARGO_BIN_EXE_riff")) + .args(["--color=on", "--no-pager", "left", "right"]) + .current_dir(jj_temp.path()) + .env("PWD", repo.path()) + .output() + .unwrap(); + let stdout = String::from_utf8(output.stdout).unwrap(); + + let repo_url = file_url(&repo.path().join("m.c")); + assert!( + stdout.contains(&format!("{}#6", repo_url)), + "Expected a link to {}#6 in:\n{}", + repo_url, + stdout + ); + + let temp_dir_url = file_url(jj_temp.path()); + assert!( + !stdout.contains(&temp_dir_url), + "Expected no links into {} in:\n{}", + temp_dir_url, + stdout + ); +} + +#[test] +fn test_links_relative_to_cwd_when_not_found_under_pwd() { + // This is what "cargo test" does when run from a subdirectory: PWD is the + // subdirectory, cwd is the package root. + let cwd = tempfile::tempdir().unwrap(); + write(&cwd.path().join("m.c"), NEW_C); + let pwd = tempfile::tempdir().unwrap(); + + let diff = "--- a/m.c\n+++ b/m.c\n@@ -2,6 +2,6 @@ int main() {\n 1;\n 2;\n 3;\n 4;\n- 5;\n+ 50;\n }\n"; + + let mut child = Command::new(env!("CARGO_BIN_EXE_riff")) + .args(["--color=on", "--no-pager"]) + .current_dir(cwd.path()) + .env("PWD", pwd.path()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + child + .stdin + .take() + .unwrap() + .write_all(diff.as_bytes()) + .unwrap(); + let output = child.wait_with_output().unwrap(); + let stdout = String::from_utf8(output.stdout).unwrap(); + + let cwd_url = file_url(&cwd.path().join("m.c")); + assert!( + stdout.contains(&cwd_url), + "Expected a link to {} in:\n{}", + cwd_url, + stdout + ); +} ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/riff/vendor.tar.zst /work/SRC/openSUSE:Factory/.riff.new.3759290/vendor.tar.zst differ: char 7, line 1
