pesde/src/patches.rs

182 lines
5.9 KiB
Rust
Raw Normal View History

use crate::{
lockfile::DownloadedGraph, source::traits::PackageRef, Project, MANIFEST_FILE_NAME,
PACKAGES_CONTAINER_NAME,
};
2024-11-05 19:44:24 +00:00
use fs_err::tokio as fs;
use git2::{ApplyLocation, Diff, DiffFormat, DiffLineType, Repository, Signature};
2024-07-28 17:19:54 +01:00
use relative_path::RelativePathBuf;
2024-11-01 19:57:32 +00:00
use std::path::Path;
2024-07-23 15:37:47 +01:00
2024-08-03 21:18:38 +01:00
/// Set up a git repository for patches
2024-07-23 15:37:47 +01:00
pub fn setup_patches_repo<P: AsRef<Path>>(dir: P) -> Result<Repository, git2::Error> {
let repo = Repository::init(&dir)?;
{
let signature = Signature::now(
env!("CARGO_PKG_NAME"),
concat!(env!("CARGO_PKG_NAME"), "@localhost"),
)?;
let mut index = repo.index()?;
index.add_all(["*"], git2::IndexAddOption::DEFAULT, None)?;
index.write()?;
let oid = index.write_tree()?;
let tree = repo.find_tree(oid)?;
repo.commit(
Some("HEAD"),
&signature,
&signature,
"begin patch",
&tree,
&[],
)?;
}
Ok(repo)
}
2024-08-03 21:18:38 +01:00
/// Create a patch from the current state of the repository
2024-07-23 15:37:47 +01:00
pub fn create_patch<P: AsRef<Path>>(dir: P) -> Result<Vec<u8>, git2::Error> {
let mut patches = vec![];
let repo = Repository::open(dir.as_ref())?;
let original = repo.head()?.peel_to_tree()?;
// reset the manifest file to the original state
let mut checkout_builder = git2::build::CheckoutBuilder::new();
checkout_builder.force();
checkout_builder.path(MANIFEST_FILE_NAME);
repo.checkout_tree(original.as_object(), Some(&mut checkout_builder))?;
let diff = repo.diff_tree_to_workdir(Some(&original), None)?;
diff.print(DiffFormat::Patch, |_delta, _hunk, line| {
if matches!(
line.origin_value(),
DiffLineType::Context | DiffLineType::Addition | DiffLineType::Deletion
) {
let origin = line.origin();
let mut buffer = vec![0; origin.len_utf8()];
origin.encode_utf8(&mut buffer);
patches.extend(buffer);
}
patches.extend(line.content());
true
})?;
Ok(patches)
}
impl Project {
2024-08-03 21:18:38 +01:00
/// Apply patches to the project's dependencies
2024-11-05 19:44:24 +00:00
pub async fn apply_patches(
&self,
graph: &DownloadedGraph,
) -> Result<(), errors::ApplyPatchesError> {
let manifest = self.deser_manifest().await?;
2024-07-23 15:37:47 +01:00
for (name, versions) in manifest.patches {
for (version_id, patch_path) in versions {
let patch_path = patch_path.to_path(self.package_dir());
2024-11-01 19:57:32 +00:00
let patch = Diff::from_buffer(
2024-11-05 19:44:24 +00:00
&fs::read(&patch_path)
.await
.map_err(errors::ApplyPatchesError::PatchRead)?,
2024-11-01 19:57:32 +00:00
)?;
2024-07-23 15:37:47 +01:00
let Some(node) = graph
.get(&name)
.and_then(|versions| versions.get(&version_id))
else {
2024-09-04 18:48:37 +01:00
log::warn!(
"patch for {name}@{version_id} not applied because it is not in the graph"
);
continue;
2024-07-23 15:37:47 +01:00
};
let container_folder = node.node.container_folder(
&self
.package_dir()
.join(
manifest
.target
.kind()
.packages_folder(&node.node.pkg_ref.target_kind()),
)
2024-07-23 15:37:47 +01:00
.join(PACKAGES_CONTAINER_NAME),
&name,
version_id.version(),
);
2024-07-23 23:53:34 +01:00
log::debug!("applying patch to {name}@{version_id}");
2024-07-23 15:37:47 +01:00
{
let repo = setup_patches_repo(&container_folder)?;
2024-11-05 19:44:24 +00:00
for delta in patch.deltas() {
2024-07-28 17:19:54 +01:00
if !matches!(delta.status(), git2::Delta::Modified) {
2024-11-05 19:44:24 +00:00
continue;
2024-07-28 17:19:54 +01:00
}
let file = delta.new_file();
let Some(relative_path) = file.path() else {
2024-11-05 19:44:24 +00:00
continue;
2024-07-28 17:19:54 +01:00
};
let relative_path = RelativePathBuf::from_path(relative_path).unwrap();
let path = relative_path.to_path(&container_folder);
if !path.is_file() {
2024-11-05 19:44:24 +00:00
continue;
2024-07-28 17:19:54 +01:00
}
// there is no way (as far as I know) to check if it's hardlinked
// so, we always unlink it
2024-11-05 19:44:24 +00:00
let content = fs::read(&path).await.unwrap();
fs::remove_file(&path).await.unwrap();
fs::write(path, content).await.unwrap();
}
2024-07-28 17:19:54 +01:00
2024-11-05 19:44:24 +00:00
repo.apply(&patch, ApplyLocation::Both, None)?;
2024-07-23 15:37:47 +01:00
}
2024-07-23 23:53:34 +01:00
log::debug!("patch applied to {name}@{version_id}, removing .git directory");
2024-11-05 19:44:24 +00:00
fs::remove_dir_all(container_folder.join(".git"))
.await
2024-11-01 19:57:32 +00:00
.map_err(errors::ApplyPatchesError::DotGitRemove)?;
2024-07-23 15:37:47 +01:00
}
}
Ok(())
}
}
2024-08-03 21:18:38 +01:00
/// Errors that can occur when using patches
2024-07-23 15:37:47 +01:00
pub mod errors {
use thiserror::Error;
2024-08-03 21:18:38 +01:00
/// Errors that can occur when applying patches
2024-07-23 15:37:47 +01:00
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ApplyPatchesError {
2024-08-03 21:18:38 +01:00
/// Error deserializing the project manifest
2024-07-23 15:37:47 +01:00
#[error("error deserializing project manifest")]
ManifestDeserializationFailed(#[from] crate::errors::ManifestReadError),
2024-08-03 21:18:38 +01:00
/// Error interacting with git
2024-07-23 15:37:47 +01:00
#[error("error interacting with git")]
2024-11-01 19:57:32 +00:00
Git(#[from] git2::Error),
2024-07-23 15:37:47 +01:00
2024-08-03 21:18:38 +01:00
/// Error reading the patch file
2024-11-01 19:57:32 +00:00
#[error("error reading patch file")]
PatchRead(#[source] std::io::Error),
2024-07-23 15:37:47 +01:00
2024-08-03 21:18:38 +01:00
/// Error removing the .git directory
2024-07-23 15:37:47 +01:00
#[error("error removing .git directory")]
2024-11-01 19:57:32 +00:00
DotGitRemove(#[source] std::io::Error),
2024-07-23 15:37:47 +01:00
}
}