From 0f00f0425aaa6366d78dc8cfa2249fa5f11a2661 Mon Sep 17 00:00:00 2001 From: Kiril Videlov Date: Sun, 21 Jul 2024 16:15:41 +0200 Subject: [PATCH] move tree writing out of virtual branch actions crate --- Cargo.lock | 2 + crates/gitbutler-branch-actions/src/base.rs | 3 +- .../src/branch_manager/branch_removal.rs | 9 +- crates/gitbutler-branch-actions/src/status.rs | 7 +- .../gitbutler-branch-actions/src/virtual.rs | 293 ++---------------- crates/gitbutler-diff/Cargo.toml | 2 + crates/gitbutler-diff/src/lib.rs | 1 + crates/gitbutler-diff/src/write.rs | 263 ++++++++++++++++ 8 files changed, 303 insertions(+), 277 deletions(-) create mode 100644 crates/gitbutler-diff/src/write.rs diff --git a/Cargo.lock b/Cargo.lock index 700b63cba..70dad6751 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2086,7 +2086,9 @@ version = "0.0.0" dependencies = [ "anyhow", "bstr", + "diffy", "git2", + "gitbutler-command-context", "gitbutler-serde", "hex", "md5", diff --git a/crates/gitbutler-branch-actions/src/base.rs b/crates/gitbutler-branch-actions/src/base.rs index 427434f6c..65a2705ca 100644 --- a/crates/gitbutler-branch-actions/src/base.rs +++ b/crates/gitbutler-branch-actions/src/base.rs @@ -14,7 +14,6 @@ use gitbutler_reference::{Refname, RemoteRefname}; use gitbutler_repo::{LogUntil, RepoActionsExt, RepositoryExt}; use serde::Serialize; -use super::r#virtual as vb; use crate::branch_manager::BranchManagerExt; use crate::conflicts::RepoConflictsExt; use crate::integration::update_gitbutler_integration; @@ -246,7 +245,7 @@ pub(crate) fn set_base_branch( created_timestamp_ms: now_ms, updated_timestamp_ms: now_ms, head: current_head_commit.id(), - tree: vb::write_hunks_onto_commit( + tree: gitbutler_diff::write::hunks_onto_commit( project_repository, current_head_commit.id(), gitbutler_diff::diff_files_into_hunks(wd_diff), diff --git a/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs b/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs index b82d2abef..b0c9089f6 100644 --- a/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs +++ b/crates/gitbutler-branch-actions/src/branch_manager/branch_removal.rs @@ -2,7 +2,7 @@ use crate::{ conflicts::{self}, ensure_selected_for_changes, get_applied_status, integration::get_integration_commiter, - write_hunks_onto_oid, NameConflictResolution, VirtualBranchesExt, + NameConflictResolution, VirtualBranchesExt, }; use anyhow::{anyhow, Context, Result}; use git2::build::TreeUpdateBuilder; @@ -87,8 +87,11 @@ impl BranchManager<'_> { |final_tree, status| { let final_tree = final_tree?; let branch = status.0; - let tree_oid = - write_hunks_onto_oid(self.project_repository, &branch.head, status.1)?; + let tree_oid = gitbutler_diff::write::hunks_onto_oid( + self.project_repository, + &branch.head, + status.1, + )?; let branch_tree = repo.find_tree(tree_oid)?; let mut result = repo.merge_trees(&base_tree, &final_tree, &branch_tree, None)?; diff --git a/crates/gitbutler-branch-actions/src/status.rs b/crates/gitbutler-branch-actions/src/status.rs index dfcc9b149..11a02d2e6 100644 --- a/crates/gitbutler-branch-actions/src/status.rs +++ b/crates/gitbutler-branch-actions/src/status.rs @@ -15,8 +15,8 @@ use std::{ }; use crate::{ - conflicts::RepoConflictsExt, integration::get_workspace_head, write_hunks_onto_oid, - BranchManagerExt, HunkLock, MTimeCache, VirtualBranchHunk, VirtualBranchesExt, + conflicts::RepoConflictsExt, integration::get_workspace_head, BranchManagerExt, HunkLock, + MTimeCache, VirtualBranchHunk, VirtualBranchesExt, }; pub struct VirtualBranchesStatus { @@ -189,7 +189,8 @@ pub fn get_applied_status( if !project_repository.is_resolving() { let vb_state = project_repository.project().virtual_branches(); for (vbranch, files) in &mut hunks_by_branch { - vbranch.tree = write_hunks_onto_oid(project_repository, &vbranch.head, files)?; + vbranch.tree = + gitbutler_diff::write::hunks_onto_oid(project_repository, &vbranch.head, files)?; vb_state .set_branch(vbranch.clone()) .context(format!("failed to write virtual branch {}", vbranch.name))?; diff --git a/crates/gitbutler-branch-actions/src/virtual.rs b/crates/gitbutler-branch-actions/src/virtual.rs index 435f0e945..4250f2d31 100644 --- a/crates/gitbutler-branch-actions/src/virtual.rs +++ b/crates/gitbutler-branch-actions/src/virtual.rs @@ -10,9 +10,7 @@ use gitbutler_diff::{Hunk, HunkHash}; use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname}; use gitbutler_repo::credentials::Helper; use gitbutler_repo::{LogUntil, RepoActionsExt, RepositoryExt}; -use std::borrow::{Borrow, Cow}; -#[cfg(target_family = "unix")] -use std::os::unix::prelude::PermissionsExt; +use std::borrow::Cow; use std::time::SystemTime; use std::{ collections::HashMap, @@ -21,10 +19,8 @@ use std::{ }; use anyhow::{anyhow, bail, Context, Result}; -use bstr::{BString, ByteSlice, ByteVec}; -use diffy::{apply_bytes as diffy_apply, Line, Patch}; +use bstr::{BString, ByteSlice}; use git2_hooks::HookResult; -use hex::ToHex; use serde::{Deserialize, Serialize}; use crate::author::Author; @@ -269,8 +265,11 @@ pub fn unapply_ownership( target_commit.tree().context("failed to get target tree"), |final_tree, status| { let final_tree = final_tree?; - let tree_oid = - write_hunks_onto_oid(project_repository, &integration_commit_id, status.1)?; + let tree_oid = gitbutler_diff::write::hunks_onto_oid( + project_repository, + &integration_commit_id, + status.1, + )?; let branch_tree = repo.find_tree(tree_oid)?; let mut result = repo.merge_trees(&base_tree, &final_tree, &branch_tree, None)?; let final_tree_oid = result.write_tree_to(project_repository.repo())?; @@ -279,7 +278,8 @@ pub fn unapply_ownership( }, )?; - let final_tree_oid = write_hunks_onto_tree(project_repository, &final_tree, diff)?; + let final_tree_oid = + gitbutler_diff::write::hunks_onto_tree(project_repository, &final_tree, diff)?; let final_tree = repo .find_tree(final_tree_oid) .context("failed to find tree")?; @@ -1125,195 +1125,6 @@ fn diffs_into_virtual_files( virtual_hunks_into_virtual_files(project_repository, hunks_by_filepath) } -// this function takes a list of file ownership, -// constructs a tree from those changes on top of the target -// and writes it as a new tree for storage -pub(crate) fn write_hunks_onto_oid( - project_repository: &ProjectRepository, - target: &git2::Oid, - files: impl IntoIterator, impl Borrow>)>, -) -> Result -where - T: Into + Clone, -{ - write_hunks_onto_commit(project_repository, *target, files) -} - -pub(crate) fn write_hunks_onto_commit( - project_repository: &ProjectRepository, - commit_oid: git2::Oid, - files: impl IntoIterator, impl Borrow>)>, -) -> Result -where - T: Into + Clone, -{ - // read the base sha into an index - let git_repository: &git2::Repository = project_repository.repo(); - - let head_commit = git_repository.find_commit(commit_oid)?; - let base_tree = head_commit.tree()?; - - write_hunks_onto_tree(project_repository, &base_tree, files) -} - -pub(crate) fn write_hunks_onto_tree( - project_repository: &ProjectRepository, - base_tree: &git2::Tree, - files: impl IntoIterator, impl Borrow>)>, -) -> Result -where - T: Into + Clone, -{ - let git_repository = project_repository.repo(); - let mut builder = git2::build::TreeUpdateBuilder::new(); - // now update the index with content in the working directory for each file - for (rel_path, hunks) in files { - let rel_path = rel_path.borrow(); - let hunks: Vec = hunks.borrow().iter().map(|h| h.clone().into()).collect(); - let full_path = project_repository.project().worktree_path().join(rel_path); - - let is_submodule = full_path.is_dir() - && hunks.len() == 1 - && hunks[0].diff_lines.contains_str(b"Subproject commit"); - - // if file exists - if full_path.exists() { - // if file is executable, use 755, otherwise 644 - let mut filemode = git2::FileMode::Blob; - // check if full_path file is executable - if let Ok(metadata) = std::fs::symlink_metadata(&full_path) { - #[cfg(target_family = "unix")] - { - if metadata.permissions().mode() & 0o111 != 0 { - filemode = git2::FileMode::BlobExecutable; - } - } - - #[cfg(target_os = "windows")] - { - // NOTE: *Keep* the existing executable bit if it was present - // in the tree already, don't try to derive something from - // the FS that doesn't exist. - filemode = base_tree - .get_path(rel_path) - .ok() - .and_then(|entry| { - (entry.filemode() & 0o100000 == 0o100000 - && entry.filemode() & 0o111 != 0) - .then_some(git2::FileMode::BlobExecutable) - }) - .unwrap_or(filemode); - } - - if metadata.file_type().is_symlink() { - filemode = git2::FileMode::Link; - } - } - - // get the blob - if filemode == git2::FileMode::Link { - // it's a symlink, make the content the path of the link - let link_target = std::fs::read_link(&full_path)?; - - // if the link target is inside the project repository, make it relative - let link_target = link_target - .strip_prefix(project_repository.project().worktree_path()) - .unwrap_or(&link_target); - - let blob_oid = git_repository.blob( - link_target - .to_str() - .ok_or_else(|| { - anyhow!("path contains invalid utf-8 characters: {link_target:?}") - })? - .as_bytes(), - )?; - builder.upsert(rel_path, blob_oid, filemode); - } else if let Ok(tree_entry) = base_tree.get_path(rel_path) { - if hunks.len() == 1 && hunks[0].binary { - let new_blob_oid = &hunks[0].diff_lines; - // convert string to Oid - let new_blob_oid = new_blob_oid - .to_str() - .expect("hex-string") - .parse() - .context("failed to diff as oid")?; - builder.upsert(rel_path, new_blob_oid, filemode); - } else { - // blob from tree_entry - let blob = tree_entry - .to_object(git_repository) - .unwrap() - .peel_to_blob() - .context("failed to get blob")?; - - let blob_contents = blob.content(); - - let mut hunks = hunks.iter().collect::>(); - hunks.sort_by_key(|hunk| hunk.new_start); - let mut all_diffs = BString::default(); - for hunk in hunks { - all_diffs.push_str(&hunk.diff_lines); - } - - let patch = Patch::from_bytes(&all_diffs)?; - let blob_contents = apply(blob_contents, &patch).context(format!( - "failed to apply\n{}\nonto:\n{}", - all_diffs.as_bstr(), - blob_contents.as_bstr() - )); - - match blob_contents { - Ok(blob_contents) => { - // create a blob - let new_blob_oid = git_repository.blob(blob_contents.as_bytes())?; - // upsert into the builder - builder.upsert(rel_path, new_blob_oid, filemode); - } - Err(_) => { - // If the patch failed to apply, do nothing, this is handled elsewhere - continue; - } - } - } - } else if is_submodule { - let mut blob_contents = BString::default(); - - let mut hunks = hunks.iter().collect::>(); - hunks.sort_by_key(|hunk| hunk.new_start); - let mut all_diffs = BString::default(); - for hunk in hunks { - all_diffs.push_str(&hunk.diff_lines); - } - let patch = Patch::from_bytes(&all_diffs)?; - blob_contents = apply(&blob_contents, &patch) - .context(format!("failed to apply {}", all_diffs))?; - - // create a blob - let new_blob_oid = git_repository.blob(blob_contents.as_bytes())?; - // upsert into the builder - builder.upsert(rel_path, new_blob_oid, filemode); - } else { - // create a git blob from a file on disk - let blob_oid = git_repository - .blob_path(&full_path) - .context(format!("failed to create blob from path {:?}", &full_path))?; - builder.upsert(rel_path, blob_oid, filemode); - } - } else if base_tree.get_path(rel_path).is_ok() { - // remove file from index if it exists in the base tree - builder.remove(rel_path); - } - } - - // now write out the tree - let tree_oid = builder - .create_updated(project_repository.repo(), base_tree) - .context("failed to write updated tree")?; - - Ok(tree_oid) -} - #[allow(clippy::too_many_arguments)] pub fn commit( project_repository: &ProjectRepository, @@ -1393,9 +1204,9 @@ pub fn commit( Some((filepath, hunks)) } }); - write_hunks_onto_commit(project_repository, branch.head, files)? + gitbutler_diff::write::hunks_onto_commit(project_repository, branch.head, files)? } else { - write_hunks_onto_commit(project_repository, branch.head, files)? + gitbutler_diff::write::hunks_onto_commit(project_repository, branch.head, files)? }; let git_repository = project_repository.repo(); @@ -1758,8 +1569,11 @@ pub(crate) fn move_commit_file( let repo = project_repository.repo(); // write our new tree and commit for the new "from" commit without the moved changes - let new_from_tree_id = - write_hunks_onto_commit(project_repository, from_parent.id(), &diffs_to_keep)?; + let new_from_tree_id = gitbutler_diff::write::hunks_onto_commit( + project_repository, + from_parent.id(), + &diffs_to_keep, + )?; let new_from_tree = &repo .find_tree(new_from_tree_id) .with_context(|| "tree {new_from_tree_oid} not found")?; @@ -1825,7 +1639,11 @@ pub(crate) fn move_commit_file( // apply diffs_to_amend to the commit tree // and write a new commit with the changes we're moving - let new_tree_oid = write_hunks_onto_commit(project_repository, to_amend_oid, &diffs_to_amend)?; + let new_tree_oid = gitbutler_diff::write::hunks_onto_commit( + project_repository, + to_amend_oid, + &diffs_to_amend, + )?; let new_tree = project_repository .repo() .find_tree(new_tree_oid) @@ -1954,7 +1772,8 @@ pub(crate) fn amend( } // apply diffs_to_amend to the commit tree - let new_tree_oid = write_hunks_onto_commit(project_repository, commit_oid, &diffs_to_amend)?; + let new_tree_oid = + gitbutler_diff::write::hunks_onto_commit(project_repository, commit_oid, &diffs_to_amend)?; let new_tree = project_repository .repo() .find_tree(new_tree_oid) @@ -2457,7 +2276,7 @@ pub(crate) fn move_commit( destination_branch.ownership.put(ownership); } - let new_destination_tree_oid = write_hunks_onto_commit( + let new_destination_tree_oid = gitbutler_diff::write::hunks_onto_commit( project_repository, destination_branch.head, branch_head_diff, @@ -2490,70 +2309,6 @@ pub(crate) fn move_commit( Ok(()) } -/// Just like [`diffy::apply()`], but on error it will attach hashes of the input `base_image` and `patch`. -pub(crate) fn apply>(base_image: S, patch: &Patch<'_, [u8]>) -> Result { - fn md5_hash_hex(b: impl AsRef<[u8]>) -> String { - md5::compute(b).encode_hex() - } - - #[derive(Debug)] - #[allow(dead_code)] // Read by Debug auto-impl, which doesn't count - pub enum DebugLine { - // Note that each of these strings is a hash only - Context(String), - Delete(String), - Insert(String), - } - - impl<'a> From<&diffy::Line<'a, [u8]>> for DebugLine { - fn from(line: &Line<'a, [u8]>) -> Self { - match line { - Line::Context(s) => DebugLine::Context(md5_hash_hex(s)), - Line::Delete(s) => DebugLine::Delete(md5_hash_hex(s)), - Line::Insert(s) => DebugLine::Insert(md5_hash_hex(s)), - } - } - } - - #[derive(Debug)] - #[allow(dead_code)] // Read by Debug auto-impl, which doesn't count - struct DebugHunk { - old_range: diffy::HunkRange, - new_range: diffy::HunkRange, - lines: Vec, - } - - impl<'a> From<&diffy::Hunk<'a, [u8]>> for DebugHunk { - fn from(hunk: &diffy::Hunk<'a, [u8]>) -> Self { - Self { - old_range: hunk.old_range(), - new_range: hunk.new_range(), - lines: hunk.lines().iter().map(Into::into).collect(), - } - } - } - - #[derive(Debug)] - #[allow(dead_code)] // Read by Debug auto-impl, which doesn't count - struct DebugContext { - base_image_hash: String, - hunks: Vec, - } - - impl std::fmt::Display for DebugContext { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(self, f) - } - } - - diffy_apply(base_image.as_ref(), patch) - .with_context(|| DebugContext { - base_image_hash: md5_hash_hex(base_image), - hunks: patch.hunks().iter().map(Into::into).collect(), - }) - .map(Into::into) -} - // Goes through a set of changes and checks if conflicts are present. If no conflicts // are present in a file it will be resolved, meaning it will be removed from the // conflicts file. diff --git a/crates/gitbutler-diff/Cargo.toml b/crates/gitbutler-diff/Cargo.toml index 033f2de2b..f506fad86 100644 --- a/crates/gitbutler-diff/Cargo.toml +++ b/crates/gitbutler-diff/Cargo.toml @@ -13,6 +13,8 @@ anyhow = "1.0.86" hex = "0.4.3" tracing = "0.1.40" gitbutler-serde.workspace = true +gitbutler-command-context.workspace = true +diffy = "0.4.0" serde = { workspace = true, features = ["std"]} [[test]] diff --git a/crates/gitbutler-diff/src/lib.rs b/crates/gitbutler-diff/src/lib.rs index fd4826d9d..e26858ac1 100644 --- a/crates/gitbutler-diff/src/lib.rs +++ b/crates/gitbutler-diff/src/lib.rs @@ -1,5 +1,6 @@ mod diff; mod hunk; +pub mod write; pub use diff::{ diff_files_into_hunks, hunks_by_filepath, reverse_hunk, trees, workdir, ChangeType, FileDiff, GitHunk, diff --git a/crates/gitbutler-diff/src/write.rs b/crates/gitbutler-diff/src/write.rs new file mode 100644 index 000000000..a42454a98 --- /dev/null +++ b/crates/gitbutler-diff/src/write.rs @@ -0,0 +1,263 @@ +use anyhow::{anyhow, Context, Result}; +use bstr::{BString, ByteSlice, ByteVec}; +use diffy::{apply_bytes as diffy_apply, Line, Patch}; +use gitbutler_command_context::ProjectRepository; +use hex::ToHex; +#[cfg(target_family = "unix")] +use std::os::unix::prelude::PermissionsExt; +use std::{borrow::Borrow, path::PathBuf}; + +use crate::GitHunk; + +// this function takes a list of file ownership, +// constructs a tree from those changes on top of the target +// and writes it as a new tree for storage +pub fn hunks_onto_oid( + project_repository: &ProjectRepository, + target: &git2::Oid, + files: impl IntoIterator, impl Borrow>)>, +) -> Result +where + T: Into + Clone, +{ + hunks_onto_commit(project_repository, *target, files) +} + +pub fn hunks_onto_commit( + project_repository: &ProjectRepository, + commit_oid: git2::Oid, + files: impl IntoIterator, impl Borrow>)>, +) -> Result +where + T: Into + Clone, +{ + // read the base sha into an index + let git_repository: &git2::Repository = project_repository.repo(); + + let head_commit = git_repository.find_commit(commit_oid)?; + let base_tree = head_commit.tree()?; + + hunks_onto_tree(project_repository, &base_tree, files) +} + +pub fn hunks_onto_tree( + project_repository: &ProjectRepository, + base_tree: &git2::Tree, + files: impl IntoIterator, impl Borrow>)>, +) -> Result +where + T: Into + Clone, +{ + let git_repository = project_repository.repo(); + let mut builder = git2::build::TreeUpdateBuilder::new(); + // now update the index with content in the working directory for each file + for (rel_path, hunks) in files { + let rel_path = rel_path.borrow(); + let hunks: Vec = hunks.borrow().iter().map(|h| h.clone().into()).collect(); + let full_path = project_repository.project().worktree_path().join(rel_path); + + let is_submodule = full_path.is_dir() + && hunks.len() == 1 + && hunks[0].diff_lines.contains_str(b"Subproject commit"); + + // if file exists + if full_path.exists() { + // if file is executable, use 755, otherwise 644 + let mut filemode = git2::FileMode::Blob; + // check if full_path file is executable + if let Ok(metadata) = std::fs::symlink_metadata(&full_path) { + #[cfg(target_family = "unix")] + { + if metadata.permissions().mode() & 0o111 != 0 { + filemode = git2::FileMode::BlobExecutable; + } + } + + #[cfg(target_os = "windows")] + { + // NOTE: *Keep* the existing executable bit if it was present + // in the tree already, don't try to derive something from + // the FS that doesn't exist. + filemode = base_tree + .get_path(rel_path) + .ok() + .and_then(|entry| { + (entry.filemode() & 0o100000 == 0o100000 + && entry.filemode() & 0o111 != 0) + .then_some(git2::FileMode::BlobExecutable) + }) + .unwrap_or(filemode); + } + + if metadata.file_type().is_symlink() { + filemode = git2::FileMode::Link; + } + } + + // get the blob + if filemode == git2::FileMode::Link { + // it's a symlink, make the content the path of the link + let link_target = std::fs::read_link(&full_path)?; + + // if the link target is inside the project repository, make it relative + let link_target = link_target + .strip_prefix(project_repository.project().worktree_path()) + .unwrap_or(&link_target); + + let blob_oid = git_repository.blob( + link_target + .to_str() + .ok_or_else(|| { + anyhow!("path contains invalid utf-8 characters: {link_target:?}") + })? + .as_bytes(), + )?; + builder.upsert(rel_path, blob_oid, filemode); + } else if let Ok(tree_entry) = base_tree.get_path(rel_path) { + if hunks.len() == 1 && hunks[0].binary { + let new_blob_oid = &hunks[0].diff_lines; + // convert string to Oid + let new_blob_oid = new_blob_oid + .to_str() + .expect("hex-string") + .parse() + .context("failed to diff as oid")?; + builder.upsert(rel_path, new_blob_oid, filemode); + } else { + // blob from tree_entry + let blob = tree_entry + .to_object(git_repository) + .unwrap() + .peel_to_blob() + .context("failed to get blob")?; + + let blob_contents = blob.content(); + + let mut hunks = hunks.iter().collect::>(); + hunks.sort_by_key(|hunk| hunk.new_start); + let mut all_diffs = BString::default(); + for hunk in hunks { + all_diffs.push_str(&hunk.diff_lines); + } + + let patch = Patch::from_bytes(&all_diffs)?; + let blob_contents = apply(blob_contents, &patch).context(format!( + "failed to apply\n{}\nonto:\n{}", + all_diffs.as_bstr(), + blob_contents.as_bstr() + )); + + match blob_contents { + Ok(blob_contents) => { + // create a blob + let new_blob_oid = git_repository.blob(blob_contents.as_bytes())?; + // upsert into the builder + builder.upsert(rel_path, new_blob_oid, filemode); + } + Err(_) => { + // If the patch failed to apply, do nothing, this is handled elsewhere + continue; + } + } + } + } else if is_submodule { + let mut blob_contents = BString::default(); + + let mut hunks = hunks.iter().collect::>(); + hunks.sort_by_key(|hunk| hunk.new_start); + let mut all_diffs = BString::default(); + for hunk in hunks { + all_diffs.push_str(&hunk.diff_lines); + } + let patch = Patch::from_bytes(&all_diffs)?; + blob_contents = apply(&blob_contents, &patch) + .context(format!("failed to apply {}", all_diffs))?; + + // create a blob + let new_blob_oid = git_repository.blob(blob_contents.as_bytes())?; + // upsert into the builder + builder.upsert(rel_path, new_blob_oid, filemode); + } else { + // create a git blob from a file on disk + let blob_oid = git_repository + .blob_path(&full_path) + .context(format!("failed to create blob from path {:?}", &full_path))?; + builder.upsert(rel_path, blob_oid, filemode); + } + } else if base_tree.get_path(rel_path).is_ok() { + // remove file from index if it exists in the base tree + builder.remove(rel_path); + } + } + + // now write out the tree + let tree_oid = builder + .create_updated(project_repository.repo(), base_tree) + .context("failed to write updated tree")?; + + Ok(tree_oid) +} + +/// Just like [`diffy::apply()`], but on error it will attach hashes of the input `base_image` and `patch`. +pub fn apply>(base_image: S, patch: &Patch<'_, [u8]>) -> Result { + fn md5_hash_hex(b: impl AsRef<[u8]>) -> String { + md5::compute(b).encode_hex() + } + + #[derive(Debug)] + #[allow(dead_code)] // Read by Debug auto-impl, which doesn't count + pub enum DebugLine { + // Note that each of these strings is a hash only + Context(String), + Delete(String), + Insert(String), + } + + impl<'a> From<&diffy::Line<'a, [u8]>> for DebugLine { + fn from(line: &Line<'a, [u8]>) -> Self { + match line { + Line::Context(s) => DebugLine::Context(md5_hash_hex(s)), + Line::Delete(s) => DebugLine::Delete(md5_hash_hex(s)), + Line::Insert(s) => DebugLine::Insert(md5_hash_hex(s)), + } + } + } + + #[derive(Debug)] + #[allow(dead_code)] // Read by Debug auto-impl, which doesn't count + struct DebugHunk { + old_range: diffy::HunkRange, + new_range: diffy::HunkRange, + lines: Vec, + } + + impl<'a> From<&diffy::Hunk<'a, [u8]>> for DebugHunk { + fn from(hunk: &diffy::Hunk<'a, [u8]>) -> Self { + Self { + old_range: hunk.old_range(), + new_range: hunk.new_range(), + lines: hunk.lines().iter().map(Into::into).collect(), + } + } + } + + #[derive(Debug)] + #[allow(dead_code)] // Read by Debug auto-impl, which doesn't count + struct DebugContext { + base_image_hash: String, + hunks: Vec, + } + + impl std::fmt::Display for DebugContext { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } + } + + diffy_apply(base_image.as_ref(), patch) + .with_context(|| DebugContext { + base_image_hash: md5_hash_hex(base_image), + hunks: patch.hunks().iter().map(Into::into).collect(), + }) + .map(Into::into) +}