From 7b5d029324c85ca7e9927d3a3a036c602b78febf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20R=C3=BC=C3=9Fler?= Date: Tue, 1 Apr 2025 19:02:36 +0200 Subject: [PATCH] Draft PoC of blame using gitoxide --- Cargo.lock | 99 +++++++++++++++++--------- asyncgit/Cargo.toml | 2 + asyncgit/src/error.rs | 28 ++++++++ asyncgit/src/sync/blame.rs | 140 ++++++++++++++++++------------------- 4 files changed, 163 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b1f15ed68..eb253d1c3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.25.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -67,6 +67,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -127,9 +133,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arc-swap" @@ -180,9 +186,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.76" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", "cfg-if", @@ -190,7 +196,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-link", + "windows-targets 0.52.6", ] [[package]] @@ -291,9 +297,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", @@ -392,13 +398,14 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ + "android-tzdata", "iana-time-zone", "num-traits", - "windows-link", + "windows-link 0.1.0", ] [[package]] @@ -413,18 +420,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" dependencies = [ "anstream", "anstyle", @@ -716,7 +723,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1107,9 +1114,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.32.3" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "git-version" @@ -1227,6 +1234,7 @@ checksum = "5fd3a6fea165debe0e80648495f894aa2371a771e3ceb7a7dcc304f1c4344c43" dependencies = [ "gix-actor", "gix-attributes", + "gix-blame", "gix-command", "gix-commitgraph", "gix-config", @@ -1309,6 +1317,25 @@ dependencies = [ "thiserror", ] +[[package]] +name = "gix-blame" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "260df64cea7bf3ab6db00e8f8cd8f1f85513d69c19fadd714422a39b8e8a8617" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-diff", + "gix-hash", + "gix-object", + "gix-revwalk", + "gix-trace", + "gix-traverse", + "gix-worktree", + "smallvec", + "thiserror", +] + [[package]] name = "gix-chunk" version = "0.4.12" @@ -2244,12 +2271,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.15.2", ] [[package]] @@ -2534,9 +2561,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "lru" @@ -2694,9 +2721,9 @@ dependencies = [ [[package]] name = "object" -version = "0.37.3" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -2752,9 +2779,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.110" +version = "0.9.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" +checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847" dependencies = [ "cc", "libc", @@ -2827,7 +2854,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3246,7 +3273,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3715,7 +3742,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4127,6 +4154,12 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + [[package]] name = "windows-link" version = "0.2.1" @@ -4175,7 +4208,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -4200,7 +4233,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", diff --git a/asyncgit/Cargo.toml b/asyncgit/Cargo.toml index f1d5bb1878..8bc0fc4c70 100644 --- a/asyncgit/Cargo.toml +++ b/asyncgit/Cargo.toml @@ -25,6 +25,8 @@ fuzzy-matcher = "0.3" git2 = "0.20" git2-hooks = { path = "../git2-hooks", version = ">=0.5" } gix = { version = "0.74.1", default-features = false, features = [ + "blame", + "blob-diff", "max-performance", "revision", "mailmap", diff --git a/asyncgit/src/error.rs b/asyncgit/src/error.rs index e36040d814..a789a7efe4 100644 --- a/asyncgit/src/error.rs +++ b/asyncgit/src/error.rs @@ -7,6 +7,10 @@ use thiserror::Error; /// #[derive(Error, Debug)] pub enum GixError { + /// + #[error("gix::config::diff::algorithm error: {0}")] + ConfigDiffAlgorithm(#[from] gix::config::diff::algorithm::Error), + /// #[error("gix::discover error: {0}")] Discover(#[from] Box), @@ -47,6 +51,12 @@ pub enum GixError { #[error("gix::reference::iter::init::Error error: {0}")] ReferenceIterInit(#[from] gix::reference::iter::init::Error), + /// + #[error("gix::repository::blame_file::Error error: {0}")] + RepositoryBlameFile( + #[from] Box, + ), + /// #[error("gix::revision::walk error: {0}")] RevisionWalk(#[from] gix::revision::walk::Error), @@ -201,6 +211,12 @@ impl From> for Error { } } +impl From for Error { + fn from(error: gix::config::diff::algorithm::Error) -> Self { + Self::Gix(GixError::from(error)) + } +} + impl From for GixError { fn from(error: gix::discover::Error) -> Self { Self::Discover(Box::new(error)) @@ -348,3 +364,15 @@ impl From for Error { Self::Gix(GixError::from(error)) } } + +impl From for GixError { + fn from(error: gix::repository::blame_file::Error) -> Self { + Self::RepositoryBlameFile(Box::new(error)) + } +} + +impl From for Error { + fn from(error: gix::repository::blame_file::Error) -> Self { + Self::Gix(GixError::from(error)) + } +} diff --git a/asyncgit/src/sync/blame.rs b/asyncgit/src/sync/blame.rs index 19f125f6b3..fc7058be66 100644 --- a/asyncgit/src/sync/blame.rs +++ b/asyncgit/src/sync/blame.rs @@ -2,14 +2,12 @@ use super::{utils, CommitId, RepoPath}; use crate::{ - error::{Error, Result}, - sync::{get_commits_info, repository::repo}, + error::Result, + sync::{get_commits_info, gix_repo}, }; -use git2::BlameOptions; +use gix::blame::Options; use scopetime::scope_time; use std::collections::{HashMap, HashSet}; -use std::io::{BufRead, BufReader}; -use std::path::Path; /// A `BlameHunk` contains all the information that will be shown to the user. #[derive(Clone, Hash, Debug, PartialEq, Eq)] @@ -40,19 +38,6 @@ pub struct FileBlame { pub lines: Vec<(Option, String)>, } -/// fixup `\` windows path separators to git compatible `/` -fn fixup_windows_path(path: &str) -> String { - #[cfg(windows)] - { - path.replace('\\', "/") - } - - #[cfg(not(windows))] - { - path.to_string() - } -} - /// pub fn blame_file( repo_path: &RepoPath, @@ -61,35 +46,41 @@ pub fn blame_file( ) -> Result { scope_time!("blame_file"); - let repo = repo(repo_path)?; + let file_path: &gix::bstr::BStr = file_path.into(); + let file_path = + gix::path::to_unix_separators_on_windows(file_path); - let commit_id = if let Some(commit_id) = commit_id { - commit_id - } else { - utils::get_head_repo(&repo)? + let repo: gix::Repository = gix_repo(repo_path)?; + let tip: gix::ObjectId = match commit_id { + Some(commit_id) => gix::ObjectId::from_bytes_or_panic( + commit_id.get_oid().as_bytes(), + ), + _ => repo.head()?.peel_to_commit()?.id, }; - let spec = - format!("{}:{}", commit_id, fixup_windows_path(file_path)); + let diff_algorithm = repo.diff_algorithm()?; - let object = repo.revparse_single(&spec)?; - let blob = repo.find_blob(object.id())?; - - if blob.is_binary() { - return Err(Error::NoBlameOnBinaryFile); - } + let options: Options = Options { + diff_algorithm, + ..Options::default() + }; - let mut opts = BlameOptions::new(); - opts.newest_commit(commit_id.into()); + // TODO: `blame_file` does not take `diff_algorithm` into account. Instead, it relies on + // `#[default]` which, as of 2025-10-30, is `Histogram`. + let outcome = repo.blame_file(&file_path, tip, options)?; - let blame = - repo.blame_file(Path::new(file_path), Some(&mut opts))?; + let commit_id = if let Some(commit_id) = commit_id { + commit_id + } else { + let repo = crate::sync::repo(repo_path)?; - let reader = BufReader::new(blob.content()); + utils::get_head_repo(&repo)? + }; - let unique_commit_ids: HashSet<_> = blame + let unique_commit_ids: HashSet = outcome + .entries .iter() - .map(|hunk| CommitId::new(hunk.final_commit_id())) + .map(|entry| entry.commit_id.into()) .collect(); let mut commit_ids = Vec::with_capacity(unique_commit_ids.len()); commit_ids.extend(unique_commit_ids); @@ -100,46 +91,49 @@ pub fn blame_file( .map(|commit_info| (commit_info.id, commit_info)) .collect(); - let lines: Vec<(Option, String)> = reader - .lines() - .enumerate() - .map(|(i, line)| { - // Line indices in a `FileBlame` are 1-based. - let corresponding_hunk = blame.get_line(i + 1); - - if let Some(hunk) = corresponding_hunk { - let commit_id = CommitId::new(hunk.final_commit_id()); - // Line indices in a `BlameHunk` are 1-based. - let start_line = - hunk.final_start_line().saturating_sub(1); - let end_line = - start_line.saturating_add(hunk.lines_in_hunk()); - - if let Some(commit_info) = - unique_commit_infos.get(&commit_id) - { - let hunk = BlameHunk { - commit_id, - author: commit_info.author.clone(), - time: commit_info.time, - start_line, - end_line, - }; - - return ( - Some(hunk), - line.unwrap_or_else(|_| String::new()), - ); - } - } - - (None, line.unwrap_or_else(|_| String::new())) + // TODO + // The shape of data as returned by `entries_with_lines` is preferable to the one chosen here + // because the former is much closer to what the UI is going to need in the end. + let lines: Vec<(Option, String)> = outcome + .entries_with_lines() + .flat_map(|(entry, lines)| { + let commit_id = entry.commit_id.into(); + let start_in_blamed_file = + entry.start_in_blamed_file as usize; + + lines + .iter() + .enumerate() + .map(|(i, line)| { + // TODO + let trimmed_line = + line.to_string().trim_end().to_string(); + + if let Some(commit_info) = + unique_commit_infos.get(&commit_id) + { + return ( + Some(BlameHunk { + commit_id, + author: commit_info.author.clone(), + time: commit_info.time, + start_line: start_in_blamed_file + i, + end_line: start_in_blamed_file + + i + 1, + }), + trimmed_line, + ); + } + + (None, trimmed_line) + }) + .collect::>() }) .collect(); let file_blame = FileBlame { commit_id, - path: file_path.into(), + path: file_path.to_string(), lines, };