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-08-17 16:59:28 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/riff (Old) and /work/SRC/openSUSE:Factory/.riff.new.1258 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "riff" Mon Aug 17 16:59:28 2026 rev:29 rq:1371510 version:3.6.2 Changes: -------- --- /work/SRC/openSUSE:Factory/riff/riff.changes 2026-01-19 18:42:09.920138328 +0100 +++ /work/SRC/openSUSE:Factory/.riff.new.1258/riff.changes 2026-08-17 17:02:51.284219112 +0200 @@ -1,0 +2,7 @@ +Mon Aug 17 06:22:45 UTC 2026 - Michael Vetter <[email protected]> + +- Update to 3.6.2: + * The file header hyperlink pointed to the first line of the file. + Now it points to the first change in the file. + +------------------------------------------------------------------- Old: ---- riff-3.6.1.tar.zst New: ---- riff-3.6.2.tar.zst ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ riff.spec ++++++ --- /var/tmp/diff_new_pack.vXjVdy/_old 2026-08-17 17:02:52.404258653 +0200 +++ /var/tmp/diff_new_pack.vXjVdy/_new 2026-08-17 17:02:52.407258759 +0200 @@ -17,7 +17,7 @@ Name: riff -Version: 3.6.1 +Version: 3.6.2 Release: 0 Summary: A diff filter highlighting which line parts have changed License: MIT ++++++ _service ++++++ --- /var/tmp/diff_new_pack.vXjVdy/_old 2026-08-17 17:02:52.465260804 +0200 +++ /var/tmp/diff_new_pack.vXjVdy/_new 2026-08-17 17:02:52.469260946 +0200 @@ -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.1</param> + <param name="revision">3.6.2</param> <param name="match-tag">*</param> <param name="versionrewrite-pattern">v(\d+\.\d+\.\d+)</param> <param name="versionrewrite-replacement">\1</param> ++++++ riff-3.6.1.tar.zst -> riff-3.6.2.tar.zst ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.1/Cargo.lock new/riff-3.6.2/Cargo.lock --- old/riff-3.6.1/Cargo.lock 2025-11-17 20:58:21.000000000 +0100 +++ new/riff-3.6.2/Cargo.lock 2026-08-15 23:27:15.000000000 +0200 @@ -113,9 +113,9 @@ [[package]] name = "bitflags" -version = "1.3.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bytecount" @@ -210,22 +210,19 @@ [[package]] name = "errno" -version = "0.3.5" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] name = "fastrand" -version = "1.9.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "form_urlencoded" @@ -237,6 +234,18 @@ ] [[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] name = "gimli" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -387,25 +396,6 @@ ] [[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" -dependencies = [ - "libc", - "windows-sys 0.45.0", -] - -[[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -422,15 +412,15 @@ [[package]] name = "libc" -version = "0.2.140" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "linux-raw-sys" -version = "0.1.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -543,13 +533,10 @@ ] [[package]] -name = "redox_syscall" -version = "0.2.10" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" -dependencies = [ - "bitflags", -] +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "regex" @@ -570,7 +557,7 @@ [[package]] name = "riffdiff" -version = "3.6.1" +version = "3.6.2" dependencies = [ "backtrace", "base64", @@ -598,16 +585,15 @@ [[package]] name = "rustix" -version = "0.36.16" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6da3636faa25820d8648e0e31c5d519bbb01f72fdf57131f0f5f7da5fed36eab" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys 0.59.0", ] [[package]] @@ -695,15 +681,15 @@ [[package]] name = "tempfile" -version = "3.4.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ - "cfg-if", "fastrand", - "redox_syscall", + "getrandom", + "once_cell", "rustix", - "windows-sys 0.42.0", + "windows-sys 0.59.0", ] [[package]] @@ -761,6 +747,15 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -784,30 +779,6 @@ [[package]] name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.1", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm 0.42.1", - "windows_x86_64_msvc 0.42.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.1", -] - -[[package]] -name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" @@ -826,21 +797,6 @@ [[package]] name = "windows-targets" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.1", - "windows_aarch64_msvc 0.42.1", - "windows_i686_gnu 0.42.1", - "windows_i686_msvc 0.42.1", - "windows_x86_64_gnu 0.42.1", - "windows_x86_64_gnullvm 0.42.1", - "windows_x86_64_msvc 0.42.1", -] - -[[package]] -name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" @@ -872,12 +828,6 @@ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" - -[[package]] -name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" @@ -890,12 +840,6 @@ [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" - -[[package]] -name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" @@ -908,12 +852,6 @@ [[package]] name = "windows_i686_gnu" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" - -[[package]] -name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" @@ -932,12 +870,6 @@ [[package]] name = "windows_i686_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" - -[[package]] -name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" @@ -950,12 +882,6 @@ [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" - -[[package]] -name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" @@ -968,12 +894,6 @@ [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" - -[[package]] -name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" @@ -986,12 +906,6 @@ [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" - -[[package]] -name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" @@ -1003,6 +917,12 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] name = "writeable" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.1/Cargo.toml new/riff-3.6.2/Cargo.toml --- old/riff-3.6.1/Cargo.toml 2025-11-17 20:58:21.000000000 +0100 +++ new/riff-3.6.2/Cargo.toml 2026-08-15 23:27:15.000000000 +0200 @@ -2,7 +2,7 @@ [package] name = "riffdiff" # Actually "riff", but that was already taken on crates.io -version = "3.6.1" +version = "3.6.2" 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.1/README.md new/riff-3.6.2/README.md --- old/riff-3.6.1/README.md 2025-11-17 20:58:21.000000000 +0100 +++ new/riff-3.6.2/README.md 2026-08-15 23:27:15.000000000 +0200 @@ -55,10 +55,10 @@ brew install riff ``` -## With [Archlinux User Repository (AUR)](https://aur.archlinux.org/packages/riffdiff) +## With [Archlinux User Repository (AUR)](https://aur.archlinux.org/packages/riffdiff-bin) ``` -paru -S riffdiff +paru -S riffdiff-bin ``` ## From [the Rust Crate](https://crates.io/crates/riffdiff) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.1/release.sh new/riff-3.6.2/release.sh --- old/riff-3.6.1/release.sh 2025-11-17 20:58:21.000000000 +0100 +++ new/riff-3.6.2/release.sh 2026-08-15 23:27:15.000000000 +0200 @@ -15,7 +15,7 @@ set -eu -o pipefail # List available SDKs using "xcodebuild -showsdks" -CROSSBUILD_MACOS_SDK="macosx26.1" +CROSSBUILD_MACOS_SDK="macosx26.2" # Git hooks can use this variable to avoid duplicating the CI work we do in here # anyway. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.1/src/file_highlighter.rs new/riff-3.6.2/src/file_highlighter.rs --- old/riff-3.6.1/src/file_highlighter.rs 2025-11-17 20:58:21.000000000 +0100 +++ new/riff-3.6.2/src/file_highlighter.rs 2026-08-15 23:27:15.000000000 +0200 @@ -10,6 +10,7 @@ render, Style, StyledToken, LINE_STYLE_NEW_FILENAME, LINE_STYLE_OLD_FILENAME, }; +use crate::hunk_header::HunkHeader; use crate::hunk_highlighter::HunkLinesHighlighter; use crate::refiner::Formatter; @@ -28,6 +29,13 @@ /// URL to the file we're currently highlighting, if any url: Option<url::Url>, + + /// Set once the `--- ` / `+++ ` header has been emitted. + /// + /// Emitting the header is deferred by one line so that, if that next + /// line is a hunk header, the header's own hyperlink can point at the + /// same line as the hunk header's hyperlink. + header_rendered: bool, } /// Remove trailing diff timestamp from a string, retaining only the filename @@ -51,7 +59,9 @@ self.url = hyperlink_filename(without_timestamp(new_name)); return Ok(Response { line_accepted: LineAcceptance::AcceptedWantMore, - highlighted: vec![StringFuture::from_string(self.highlighted())], + // The header itself is rendered from the next line, once + // we know whether it's a hunk header to link to. + highlighted: vec![], }); } return Err("--- was not followed by +++".to_string()); @@ -59,9 +69,42 @@ // We are now past the --- / +++ headers and are dealing with the body let mut highlights: Vec<StringFuture> = Vec::new(); + + if !self.header_rendered { + // This is the first body line. Parse it as a hunk header once, + // both to pick the header's link line and, if it is one, to + // become the hunk sub-highlighter below (avoiding parsing it + // twice). + let parsed_hunk_header = HunkHeader::parse(line); + let first_hunk_line = match &parsed_hunk_header { + Some(hunk_header) => Some(hunk_header.first_modified_line()?), + None => None, + }; + highlights.push(StringFuture::from_string(self.highlighted(first_hunk_line))); + self.header_rendered = true; + + return match parsed_hunk_header { + Some(hunk_header) => { + self.sub_highlighter = Some(Box::new(HunkLinesHighlighter::from_parsed( + hunk_header, + self.formatter.clone(), + &self.url, + )?)); + Ok(Response { + line_accepted: LineAcceptance::AcceptedWantMore, + highlighted: highlights, + }) + } + None => Ok(Response { + line_accepted: LineAcceptance::RejectedDone, + highlighted: highlights, + }), + }; + } + if let Some(ref mut highlighter) = self.sub_highlighter { let resp = highlighter.consume_line(line, thread_pool)?; - highlights = resp.highlighted; + highlights.extend(resp.highlighted); match resp.line_accepted { LineAcceptance::AcceptedWantMore => { return Ok(Response { @@ -106,10 +149,20 @@ if self.new_name.is_empty() { return Err("Input ended early, --- should have been followed by +++".to_string()); } + + let mut return_me = vec![]; + if !self.header_rendered { + // No hunk ever showed up, e.g. an empty file being added/deleted, + // or a truncated diff. Fall back to a plain, line-less link. + return_me.push(StringFuture::from_string(self.highlighted(None))); + self.header_rendered = true; + } + if let Some(ref mut sub) = self.sub_highlighter { - return sub.consume_eof(thread_pool); + return_me.extend(sub.consume_eof(thread_pool)?); } - Ok(vec![]) + + Ok(return_me) } } @@ -131,12 +184,13 @@ formatter, sub_highlighter: None, url: None, // Will be set in consume_line() based on the +++ line + header_rendered: false, }; return Some(highlighter); } - fn highlighted(&self) -> String { + fn highlighted(&self, first_hunk_line: Option<usize>) -> String { let (mut old_tokens, mut new_tokens) = diff(&self.old_name, &self.new_name); // New file @@ -165,7 +219,7 @@ None }; - decorate_paths(&mut old_tokens, &mut new_tokens); + decorate_paths(&mut old_tokens, &mut new_tokens, first_hunk_line); if let Some(prefix) = new_prefix { new_tokens.insert(0, prefix); @@ -230,7 +284,11 @@ return None; } -fn hyperlink_tokenized(just_path: &mut [StyledToken], just_filename: &mut [StyledToken]) { +fn hyperlink_tokenized( + just_path: &mut [StyledToken], + just_filename: &mut [StyledToken], + fragment_line: Option<usize>, +) { // Convert filename_tokens into a String let mut filename = String::new(); for token in just_path.iter() { @@ -240,7 +298,11 @@ filename.push_str(&token.token); } - if let Some(url) = hyperlink_filename(&filename) { + if let Some(mut url) = hyperlink_filename(&filename) { + if let Some(fragment_line) = fragment_line { + url.set_fragment(Some(&fragment_line.to_string())); + } + // Actually link the tokens for token in just_path.iter_mut() { token.url = Some(url.clone()); @@ -401,7 +463,11 @@ return (old_has_git_prefix || old_is_absolute) && (new_has_git_prefix || new_is_absolute); } -pub(crate) fn decorate_paths(old_tokens: &mut [StyledToken], new_tokens: &mut [StyledToken]) { +pub(crate) fn decorate_paths( + old_tokens: &mut [StyledToken], + new_tokens: &mut [StyledToken], + fragment_line: Option<usize>, +) { let look_for_git_prefixes = have_git_prefixes(old_tokens, new_tokens); let old_split = split_row(look_for_git_prefixes, old_tokens); @@ -425,9 +491,9 @@ if old_split.just_path == new_split.just_path && old_split.just_filename == new_split.just_filename { - hyperlink_tokenized(old_split.just_path, old_split.just_filename); + hyperlink_tokenized(old_split.just_path, old_split.just_filename, fragment_line); } - hyperlink_tokenized(new_split.just_path, new_split.just_filename); + hyperlink_tokenized(new_split.just_path, new_split.just_filename, fragment_line); lowlight_dev_null(old_split.just_path, old_split.just_filename); lowlight_dev_null(new_split.just_path, new_split.just_filename); @@ -471,13 +537,18 @@ use crate::refiner::tests::FORMATTER; fn highlight_header_lines(old_line: &str, new_line: &str) -> String { + let thread_pool = ThreadPool::new(1); let mut test_me = FileHighlighter::from_line(old_line, FORMATTER.clone()).unwrap(); - let response = test_me.consume_line(new_line, &ThreadPool::new(1)).unwrap(); + let response = test_me.consume_line(new_line, &thread_pool).unwrap(); assert_eq!(LineAcceptance::AcceptedWantMore, response.line_accepted); - assert_eq!(1, response.highlighted.len()); + assert_eq!(0, response.highlighted.len()); - let highlighted = response - .highlighted + // The header is rendered once we know whether the next line is a + // hunk header. These tests have no next line, so EOF flushes it. + let eof_highlighted = test_me.consume_eof(&thread_pool).unwrap(); + assert_eq!(1, eof_highlighted.len()); + + let highlighted = eof_highlighted .into_iter() .next() .unwrap() @@ -524,6 +595,32 @@ assert_eq!(canonical, expected, "Hyperlink should point to README.md"); } + /// Clicking the `--- ` / `+++ ` header should land in the same place as + /// clicking the first section header below it. + #[test] + fn test_header_links_to_first_hunk_line() { + let thread_pool = ThreadPool::new(1); + let mut test_me = FileHighlighter::from_line("--- README.md", FORMATTER.clone()).unwrap(); + + test_me.consume_line("+++ README.md", &thread_pool).unwrap(); + let response = test_me + .consume_line("@@ -10,3 +10,3 @@ Some section", &thread_pool) + .unwrap(); + + let mut highlighted = String::new(); + for mut future in response.highlighted { + highlighted.push_str(future.get()); + } + + // The section header links to the start line plus three context lines, + // and the file header should agree with it. + assert!( + highlighted.contains("README.md#13"), + "Expected a hyperlink to line 13 of README.md, got: {:?}", + highlighted + ); + } + #[test] fn test_brighten_filename() { let highlighted = highlight_header_lines("--- a/x/y/z.txt", "+++ b/x/y/z.txt"); @@ -595,7 +692,7 @@ let mut row = vec![StyledToken::new("README.md".to_string(), Style::Context)]; // Act: call the function - hyperlink_tokenized(&mut [], &mut row); + hyperlink_tokenized(&mut [], &mut row, None); // Assert: the file:/// URL points to our README.md file let url = row[0].url.as_ref().expect("Token should have a URL"); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.1/src/hunk_header.rs new/riff-3.6.2/src/hunk_header.rs --- old/riff-3.6.1/src/hunk_header.rs 2025-11-17 20:58:21.000000000 +0100 +++ new/riff-3.6.2/src/hunk_header.rs 2026-08-15 23:27:15.000000000 +0200 @@ -120,6 +120,20 @@ }) } + /// The first modified line in the new file, i.e. the line a click on this + /// hunk's title should land on. + pub fn first_modified_line(&self) -> Result<usize, String> { + // Skip this number of leading context lines. There are usually three + // context lines. If people start complaining we'll have to detect the + // actual number. + let context_lines_skip = 3; + if let Some(last_start) = self.starts.last().cloned() { + return Ok(last_start + context_lines_skip); + } + + return Err(format!("HunkHeader has no start lines: {:?}", self)); + } + /// Render into an ANSI highlighted string, not ending in a newline. pub fn render(&self, url: &Option<url::Url>) -> Result<String, String> { let mut rendered = String::new(); @@ -145,19 +159,7 @@ if let Some(title) = &self.title { rendered.push(' '); rendered.push_str(BOLD); - if let Some(last_start) = self.starts.last().cloned() { - // Skip this number of context lines to end up at the first - // modified line. There are usually three context lines. If - // people start complaining we'll have to detect the actual - // number. - let context_lines_skip = 3; - rendered.push_str(&hyperlink(title, url, last_start + context_lines_skip)); - } else { - return Err(format!( - "HunkHeader has no start lines when rendering title: {:?}", - self - )); - } + rendered.push_str(&hyperlink(title, url, self.first_modified_line()?)); } rendered.push_str(NORMAL); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.1/src/hunk_highlighter.rs new/riff-3.6.2/src/hunk_highlighter.rs --- old/riff-3.6.1/src/hunk_highlighter.rs 2025-11-17 20:58:21.000000000 +0100 +++ new/riff-3.6.2/src/hunk_highlighter.rs 2026-08-15 23:27:15.000000000 +0200 @@ -98,18 +98,28 @@ Self: Sized, { if let Some(hunk_header) = HunkHeader::parse(line) { - return Ok(Some(HunkLinesHighlighter { - hunk_header: Some(hunk_header.render(file_url)?), - remaining_line_counts: hunk_header.linecounts.clone(), - initial_line_counts: hunk_header.linecounts, - lines_highlighter: None, - formatter, - })); + return Ok(Some(Self::from_parsed(hunk_header, formatter, file_url)?)); } return Ok(None); } + /// Like `from_line()`, but for a hunk header the caller has already + /// parsed, so the same line doesn't get parsed twice. + pub(crate) fn from_parsed( + hunk_header: HunkHeader, + formatter: Formatter, + file_url: &Option<url::Url>, + ) -> Result<Self, String> { + return Ok(HunkLinesHighlighter { + hunk_header: Some(hunk_header.render(file_url)?), + remaining_line_counts: hunk_header.linecounts.clone(), + initial_line_counts: hunk_header.linecounts, + lines_highlighter: None, + formatter, + }); + } + fn consume_line_internal( &mut self, line: &str, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.1/src/line_collector.rs new/riff-3.6.2/src/line_collector.rs --- old/riff-3.6.1/src/line_collector.rs 2025-11-17 20:58:21.000000000 +0100 +++ new/riff-3.6.2/src/line_collector.rs 2026-08-15 23:27:15.000000000 +0200 @@ -111,12 +111,8 @@ // Flush outstanding lines self.drain_plain(); - if self.lines_highlighter.is_some() { - let result = self - .lines_highlighter - .as_mut() - .unwrap() - .consume_eof(&self.thread_pool); + if let Some(lines_highlighter) = self.lines_highlighter.as_mut() { + let result = lines_highlighter.consume_eof(&self.thread_pool); if let Err(error) = result { self.lines_highlighter = None; eprintln!("ERROR at end of input: {error}"); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/riff-3.6.1/src/rename_highlighter.rs new/riff-3.6.2/src/rename_highlighter.rs --- old/riff-3.6.1/src/rename_highlighter.rs 2025-11-17 20:58:21.000000000 +0100 +++ new/riff-3.6.2/src/rename_highlighter.rs 2026-08-15 23:27:15.000000000 +0200 @@ -60,7 +60,7 @@ let old_filename = render(&LINE_STYLE_OLD_FILENAME, "", &old_tokens); let new_filename = render(&LINE_STYLE_NEW_FILENAME, "", &new_tokens); - decorate_paths(&mut old_tokens, &mut new_tokens); + decorate_paths(&mut old_tokens, &mut new_tokens, None); return format!( "rename from {old_filename}{NORMAL}\n{BOLD}rename to {new_filename}{NORMAL}\n" ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/riff/vendor.tar.zst /work/SRC/openSUSE:Factory/.riff.new.1258/vendor.tar.zst differ: char 7, line 1
