diff --git a/asyncgit/src/error.rs b/asyncgit/src/error.rs index e36040d814..544572bb37 100644 --- a/asyncgit/src/error.rs +++ b/asyncgit/src/error.rs @@ -7,6 +7,25 @@ use thiserror::Error; /// #[derive(Error, Debug)] pub enum GixError { + #[error("gix::config::diff::algorithm::Error error: {0}")] + ConfigDiffAlgorithm(#[from] gix::config::diff::algorithm::Error), + + /// + #[error( + "gix::diff::blob::platform::prepare_diff::Error error: {0}" + )] + DiffBlobPlatformPrepareDiff( + #[from] gix::diff::blob::platform::prepare_diff::Error, + ), + + /// + #[error( + "gix::diff::blob::platform::set_resource::Error error: {0}" + )] + DiffBlobPlatformSetResource( + #[from] gix::diff::blob::platform::set_resource::Error, + ), + /// #[error("gix::discover error: {0}")] Discover(#[from] Box), @@ -35,6 +54,10 @@ pub enum GixError { #[from] gix::reference::find::existing::Error, ), + /// + #[error("gix::reference::head_tree::Error error: {0}")] + ReferenceHeadTree(#[from] gix::reference::head_tree::Error), + /// #[error("gix::reference::head_tree_id::Error error: {0}")] ReferenceHeadTreeId(#[from] gix::reference::head_tree_id::Error), @@ -47,6 +70,14 @@ pub enum GixError { #[error("gix::reference::iter::init::Error error: {0}")] ReferenceIterInit(#[from] gix::reference::iter::init::Error), + /// + #[error( + "gix::repository::diff_resource_cache::Error error: {0}" + )] + RepositoryDiffResourceCache( + #[from] gix::repository::diff_resource_cache::Error, + ), + /// #[error("gix::revision::walk error: {0}")] RevisionWalk(#[from] gix::revision::walk::Error), @@ -171,7 +202,7 @@ pub enum Error { /// #[error("gix error:{0}")] - Gix(#[from] GixError), + Gix(#[from] Box), /// #[error("amend error: config commit.gpgsign=true detected.\ngpg signing is not supported for amending non-last commits")] @@ -207,15 +238,31 @@ impl From for GixError { } } +impl From for Error { + fn from( + error: gix::diff::blob::platform::prepare_diff::Error, + ) -> Self { + Self::Gix(Box::new(GixError::from(error))) + } +} + +impl From for Error { + fn from( + error: gix::diff::blob::platform::set_resource::Error, + ) -> Self { + Self::Gix(Box::new(GixError::from(error))) + } +} + impl From for Error { fn from(error: gix::discover::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } impl From for Error { fn from(error: gix::head::peel::to_commit::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } @@ -225,13 +272,13 @@ impl From fn from( error: gix::object::find::existing::with_conversion::Error, ) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } impl From for Error { fn from(error: gix::objs::decode::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } @@ -243,37 +290,51 @@ impl From for GixError { impl From for Error { fn from(error: gix::pathspec::init::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } impl From for Error { fn from(error: gix::reference::find::existing::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) + } +} + +impl From for Error { + fn from(error: gix::reference::head_tree::Error) -> Self { + Self::Gix(Box::new(GixError::from(error))) } } impl From for Error { fn from(error: gix::reference::head_tree_id::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } impl From for Error { fn from(error: gix::reference::iter::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } impl From for Error { fn from(error: gix::reference::iter::init::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) + } +} + +impl From for Error { + fn from( + error: gix::repository::diff_resource_cache::Error, + ) -> Self { + Self::Gix(Box::new(GixError::from(error))) } } impl From for Error { fn from(error: gix::revision::walk::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } @@ -285,7 +346,7 @@ impl From for GixError { impl From for Error { fn from(error: gix::status::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } @@ -297,7 +358,7 @@ impl From for GixError { impl From for Error { fn from(error: gix::status::iter::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } @@ -309,7 +370,7 @@ impl From for GixError { impl From for Error { fn from(error: gix::status::into_iter::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } @@ -321,7 +382,7 @@ impl From for GixError { impl From for Error { fn from(error: gix::status::index_worktree::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } @@ -333,7 +394,7 @@ impl From for GixError { impl From for Error { fn from(error: gix::status::tree_index::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } @@ -345,6 +406,6 @@ impl From for GixError { impl From for Error { fn from(error: gix::worktree::open_index::Error) -> Self { - Self::Gix(GixError::from(error)) + Self::Gix(Box::new(GixError::from(error))) } } diff --git a/asyncgit/src/sync/diff.rs b/asyncgit/src/sync/diff.rs index d02a00a121..11f7cadd71 100644 --- a/asyncgit/src/sync/diff.rs +++ b/asyncgit/src/sync/diff.rs @@ -8,18 +8,30 @@ use super::{ CommitId, RepoPath, }; use crate::{ - error::Error, - error::Result, + error::{Error, Result}, hash, - sync::{get_stashes, repository::repo}, + sync::{get_stashes, gix_repo, repository::repo}, }; use easy_cast::Conv; use git2::{ Delta, Diff, DiffDelta, DiffFormat, DiffHunk, Patch, Repository, }; +use gix::{ + bstr::ByteSlice, + diff::blob::{ + unified_diff::{ConsumeHunk, ContextSize, NewlineSeparator}, + UnifiedDiff, + }, + ObjectId, +}; use scopetime::scope_time; use serde::{Deserialize, Serialize}; -use std::{cell::RefCell, fs, path::Path, rc::Rc}; +use std::{ + cell::RefCell, + fs, + path::{Path, PathBuf}, + rc::Rc, +}; /// type of diff of a single line #[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] @@ -200,6 +212,180 @@ pub(crate) fn get_diff_raw<'a>( Ok(diff) } +pub(crate) fn tokens_for_diffing( + data: &[u8], +) -> impl gix::diff::blob::intern::TokenSource { + gix::diff::blob::sources::byte_lines(data) +} + +impl ConsumeHunk for FileDiff { + type Out = Self; + + fn consume_hunk( + &mut self, + before_hunk_start: u32, + before_hunk_len: u32, + after_hunk_start: u32, + after_hunk_len: u32, + header: &str, + hunk: &[u8], + ) -> std::io::Result<()> { + let non_header_lines = hunk.lines().scan( + (before_hunk_start, after_hunk_start), + |(old_lineno, new_lineno), line| { + let (line_type, content, old_lineno, new_lineno) = + match line { + [b'+', rest @ ..] => { + let result = ( + DiffLineType::Add, + rest, + None, + Some(*new_lineno), + ); + *new_lineno += 1; + result + } + [b'-', rest @ ..] => { + let result = ( + DiffLineType::Delete, + rest, + Some(*old_lineno), + None, + ); + *old_lineno += 1; + result + } + [b' ', rest @ ..] => { + let result = ( + DiffLineType::None, + rest, + Some(*old_lineno), + Some(*new_lineno), + ); + *old_lineno += 1; + *new_lineno += 1; + result + } + _ => { + // Empty lines or unknown prefixes are treated as context. + let result = ( + DiffLineType::None, + line, + Some(*old_lineno), + Some(*new_lineno), + ); + *old_lineno += 1; + *new_lineno += 1; + result + } + }; + + Some(DiffLine { + position: DiffLinePosition { + old_lineno, + new_lineno, + }, + content: String::from_utf8_lossy(content) + .trim_matches(is_newline) + .into(), + line_type, + }) + }, + ); + + let mut lines = vec![DiffLine { + content: header.to_string().into(), + line_type: DiffLineType::Header, + position: DiffLinePosition { + old_lineno: None, + new_lineno: None, + }, + }]; + lines.extend(non_header_lines); + + let hunk_header = HunkHeader { + old_start: before_hunk_start, + old_lines: before_hunk_len, + new_start: after_hunk_start, + new_lines: after_hunk_len, + }; + + self.hunks.push(Hunk { + header_hash: hash(&hunk_header), + lines, + }); + + self.lines += hunk.lines().count(); + + Ok(()) + } + + fn finish(self) -> Self::Out { + self + } +} + +fn id_and_root_in_head( + gix_repo: &gix::Repository, + p: &str, +) -> (ObjectId, Option) { + let id = gix_repo + .head_tree() + .map_or(None, |head_tree| { + head_tree + .lookup_entry_by_path(p) + .ok() + .flatten() + .map(|entry| entry.object_id()) + }) + .unwrap_or(ObjectId::null(gix::hash::Kind::Sha1)); + + (id, None) +} + +fn id_and_root_in_index( + gix_repo: &gix::Repository, + p: &str, +) -> (ObjectId, Option) { + let id = gix_repo + .index() + .map_or(None, |index| { + index.entry_by_path(p.into()).map(|entry| entry.id) + }) + .unwrap_or(ObjectId::null(gix::hash::Kind::Sha1)); + + (id, None) +} + +fn file_diff_for_binary_files( + old_data: gix::diff::blob::platform::resource::Data, + new_data: gix::diff::blob::platform::resource::Data, +) -> FileDiff { + use gix::diff::blob::platform::resource::Data; + + let mut file_diff = FileDiff::default(); + + let old_size = match old_data { + Data::Missing => 0, + Data::Buffer { buf, .. } => u64::conv(buf.len()), + Data::Binary { size } => size, + }; + let new_size = match new_data { + Data::Missing => 0, + Data::Buffer { buf, .. } => u64::conv(buf.len()), + Data::Binary { size } => size, + }; + + let sizes = (old_size, new_size); + let size_delta = + (i64::conv(new_size)).saturating_sub(i64::conv(old_size)); + + file_diff.sizes = sizes; + file_diff.size_delta = size_delta; + + file_diff +} + /// returns diff of a specific file either in `stage` or workdir pub fn get_diff( repo_path: &RepoPath, @@ -209,11 +395,92 @@ pub fn get_diff( ) -> Result { scope_time!("get_diff"); - let repo = repo(repo_path)?; - let work_dir = work_dir(&repo)?; - let diff = get_diff_raw(&repo, p, stage, false, options)?; + let mut gix_repo = gix_repo(repo_path)?; + gix_repo.object_cache_size_if_unset( + gix_repo.compute_object_cache_size_for_tree_diffs( + &**gix_repo.index_or_empty()?, + ), + ); + + // TODO: + // The lower tree is `stage == true`, the upper tree is `stage == false`. + let (old_blob_id, old_root) = if stage { + id_and_root_in_head(&gix_repo, p) + } else { + id_and_root_in_index(&gix_repo, p) + }; + let (new_blob_id, new_root) = if stage { + id_and_root_in_index(&gix_repo, p) + } else { + ( + ObjectId::null(gix::hash::Kind::Sha1), + gix_repo.workdir().map(ToOwned::to_owned), + ) + }; - raw_diff_to_file_diff(&diff, work_dir) + let worktree_roots = gix::diff::blob::pipeline::WorktreeRoots { + old_root, + new_root, + }; + + let mut resource_cache = gix_repo.diff_resource_cache(gix::diff::blob::pipeline::Mode::ToGitUnlessBinaryToTextIsPresent, worktree_roots)?; + + resource_cache.set_resource( + old_blob_id, + gix::object::tree::EntryKind::Blob, + p.as_ref(), + gix::diff::blob::ResourceKind::OldOrSource, + &gix_repo.objects, + )?; + resource_cache.set_resource( + new_blob_id, + gix::object::tree::EntryKind::Blob, + p.as_ref(), + gix::diff::blob::ResourceKind::NewOrDestination, + &gix_repo.objects, + )?; + + let outcome = resource_cache.prepare_diff()?; + + let diff_algorithm = { + use gix::diff::blob::platform::prepare_diff::Operation; + + match outcome.operation { + Operation::InternalDiff { algorithm } => algorithm, + Operation::ExternalCommand { .. } => { + unreachable!("We disabled that") + } + Operation::SourceOrDestinationIsBinary => { + return Ok(file_diff_for_binary_files( + outcome.old.data, + outcome.new.data, + )); + } + } + }; + + let input = gix::diff::blob::intern::InternedInput::new( + tokens_for_diffing( + outcome.old.data.as_slice().unwrap_or_default(), + ), + tokens_for_diffing( + outcome.new.data.as_slice().unwrap_or_default(), + ), + ); + + let context_size = options.map_or(3, |opts| opts.context); + + let unified_diff = UnifiedDiff::new( + &input, + FileDiff::default(), + NewlineSeparator::AfterHeaderAndLine("\n"), + ContextSize::symmetrical(context_size), + ); + + let file_diff = + gix::diff::blob::diff(diff_algorithm, &input, unified_diff)?; + + Ok(file_diff) } /// returns diff of a specific file inside a commit