move tree writing out of virtual branch actions crate

This commit is contained in:
Kiril Videlov 2024-07-21 16:15:41 +02:00
parent 38e01c931c
commit 0f00f0425a
No known key found for this signature in database
GPG Key ID: A4C733025427C471
8 changed files with 303 additions and 277 deletions

2
Cargo.lock generated
View File

@ -2086,7 +2086,9 @@ version = "0.0.0"
dependencies = [
"anyhow",
"bstr",
"diffy",
"git2",
"gitbutler-command-context",
"gitbutler-serde",
"hex",
"md5",

View File

@ -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),

View File

@ -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)?;

View File

@ -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))?;

View File

@ -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<T>(
project_repository: &ProjectRepository,
target: &git2::Oid,
files: impl IntoIterator<Item = (impl Borrow<PathBuf>, impl Borrow<Vec<T>>)>,
) -> Result<git2::Oid>
where
T: Into<gitbutler_diff::GitHunk> + Clone,
{
write_hunks_onto_commit(project_repository, *target, files)
}
pub(crate) fn write_hunks_onto_commit<T>(
project_repository: &ProjectRepository,
commit_oid: git2::Oid,
files: impl IntoIterator<Item = (impl Borrow<PathBuf>, impl Borrow<Vec<T>>)>,
) -> Result<git2::Oid>
where
T: Into<gitbutler_diff::GitHunk> + 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<T>(
project_repository: &ProjectRepository,
base_tree: &git2::Tree,
files: impl IntoIterator<Item = (impl Borrow<PathBuf>, impl Borrow<Vec<T>>)>,
) -> Result<git2::Oid>
where
T: Into<gitbutler_diff::GitHunk> + 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<GitHunk> = 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::<Vec<_>>();
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::<Vec<_>>();
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<S: AsRef<[u8]>>(base_image: S, patch: &Patch<'_, [u8]>) -> Result<BString> {
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<DebugLine>,
}
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<DebugHunk>,
}
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.

View File

@ -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]]

View File

@ -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,

View File

@ -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<T>(
project_repository: &ProjectRepository,
target: &git2::Oid,
files: impl IntoIterator<Item = (impl Borrow<PathBuf>, impl Borrow<Vec<T>>)>,
) -> Result<git2::Oid>
where
T: Into<GitHunk> + Clone,
{
hunks_onto_commit(project_repository, *target, files)
}
pub fn hunks_onto_commit<T>(
project_repository: &ProjectRepository,
commit_oid: git2::Oid,
files: impl IntoIterator<Item = (impl Borrow<PathBuf>, impl Borrow<Vec<T>>)>,
) -> Result<git2::Oid>
where
T: Into<GitHunk> + 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<T>(
project_repository: &ProjectRepository,
base_tree: &git2::Tree,
files: impl IntoIterator<Item = (impl Borrow<PathBuf>, impl Borrow<Vec<T>>)>,
) -> Result<git2::Oid>
where
T: Into<GitHunk> + 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<GitHunk> = 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::<Vec<_>>();
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::<Vec<_>>();
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<S: AsRef<[u8]>>(base_image: S, patch: &Patch<'_, [u8]>) -> Result<BString> {
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<DebugLine>,
}
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<DebugHunk>,
}
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)
}