2024-11-16 09:40:37 +00:00
|
|
|
use crate::{lockfile::DownloadedGraph, 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,
|
2024-11-10 15:43:25 +00:00
|
|
|
) -> Result<
|
2024-11-10 17:08:59 +00:00
|
|
|
tokio::sync::mpsc::Receiver<Result<String, errors::ApplyPatchesError>>,
|
2024-11-10 15:43:25 +00:00
|
|
|
errors::ApplyPatchesError,
|
|
|
|
> {
|
2024-11-05 19:44:24 +00:00
|
|
|
let manifest = self.deser_manifest().await?;
|
2024-11-10 15:43:25 +00:00
|
|
|
let (tx, rx) = tokio::sync::mpsc::channel(
|
|
|
|
manifest
|
|
|
|
.patches
|
|
|
|
.values()
|
|
|
|
.map(|v| v.len())
|
|
|
|
.sum::<usize>()
|
|
|
|
.max(1),
|
|
|
|
);
|
2024-07-23 15:37:47 +01:00
|
|
|
|
|
|
|
for (name, versions) in manifest.patches {
|
|
|
|
for (version_id, patch_path) in versions {
|
2024-11-10 15:43:25 +00:00
|
|
|
let tx = tx.clone();
|
|
|
|
|
|
|
|
let name = name.clone();
|
2024-09-03 15:01:48 +01:00
|
|
|
let patch_path = patch_path.to_path(self.package_dir());
|
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"
|
|
|
|
);
|
2024-11-10 17:08:59 +00:00
|
|
|
tx.send(Ok(format!("{name}@{version_id}"))).await.unwrap();
|
2024-09-04 18:48:37 +01:00
|
|
|
continue;
|
2024-07-23 15:37:47 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
let container_folder = node.node.container_folder(
|
|
|
|
&self
|
2024-09-03 15:01:48 +01:00
|
|
|
.package_dir()
|
2024-11-16 09:40:37 +00:00
|
|
|
.join(manifest.target.kind().packages_folder(version_id.target()))
|
2024-07-23 15:37:47 +01:00
|
|
|
.join(PACKAGES_CONTAINER_NAME),
|
|
|
|
&name,
|
|
|
|
version_id.version(),
|
|
|
|
);
|
|
|
|
|
2024-11-10 15:43:25 +00:00
|
|
|
tokio::spawn(async move {
|
|
|
|
log::debug!("applying patch to {name}@{version_id}");
|
2024-07-23 23:53:34 +01:00
|
|
|
|
2024-11-10 15:43:25 +00:00
|
|
|
let patch = match fs::read(&patch_path).await {
|
|
|
|
Ok(patch) => patch,
|
|
|
|
Err(e) => {
|
|
|
|
tx.send(Err(errors::ApplyPatchesError::PatchRead(e)))
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
return;
|
2024-07-28 17:19:54 +01:00
|
|
|
}
|
2024-11-10 15:43:25 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let patch = match Diff::from_buffer(&patch) {
|
|
|
|
Ok(patch) => patch,
|
|
|
|
Err(e) => {
|
|
|
|
tx.send(Err(errors::ApplyPatchesError::Git(e)))
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
{
|
|
|
|
let repo = match setup_patches_repo(&container_folder) {
|
|
|
|
Ok(repo) => repo,
|
|
|
|
Err(e) => {
|
|
|
|
tx.send(Err(errors::ApplyPatchesError::Git(e)))
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
return;
|
|
|
|
}
|
2024-07-28 17:19:54 +01:00
|
|
|
};
|
|
|
|
|
2024-11-10 15:43:25 +00:00
|
|
|
let modified_files = patch
|
|
|
|
.deltas()
|
|
|
|
.filter(|delta| matches!(delta.status(), git2::Delta::Modified))
|
|
|
|
.filter_map(|delta| delta.new_file().path())
|
|
|
|
.map(|path| {
|
|
|
|
RelativePathBuf::from_path(path)
|
|
|
|
.unwrap()
|
|
|
|
.to_path(&container_folder)
|
|
|
|
})
|
|
|
|
.filter(|path| path.is_file())
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
for path in modified_files {
|
|
|
|
// there is no way (as far as I know) to check if it's hardlinked
|
|
|
|
// so, we always unlink it
|
|
|
|
let content = match fs::read(&path).await {
|
|
|
|
Ok(content) => content,
|
|
|
|
Err(e) => {
|
|
|
|
tx.send(Err(errors::ApplyPatchesError::File(e)))
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if let Err(e) = fs::remove_file(&path).await {
|
|
|
|
tx.send(Err(errors::ApplyPatchesError::File(e)))
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Err(e) = fs::write(path, content).await {
|
|
|
|
tx.send(Err(errors::ApplyPatchesError::File(e)))
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
return;
|
|
|
|
}
|
2024-07-28 17:19:54 +01:00
|
|
|
}
|
|
|
|
|
2024-11-10 15:43:25 +00:00
|
|
|
if let Err(e) = repo.apply(&patch, ApplyLocation::Both, None) {
|
|
|
|
tx.send(Err(errors::ApplyPatchesError::Git(e)))
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
return;
|
|
|
|
}
|
2024-11-05 19:44:24 +00:00
|
|
|
}
|
2024-07-28 17:19:54 +01:00
|
|
|
|
2024-11-10 15:43:25 +00:00
|
|
|
log::debug!("patch applied to {name}@{version_id}, removing .git directory");
|
2024-07-23 15:37:47 +01:00
|
|
|
|
2024-11-10 15:43:25 +00:00
|
|
|
if let Err(e) = fs::remove_dir_all(container_folder.join(".git")).await {
|
|
|
|
tx.send(Err(errors::ApplyPatchesError::DotGitRemove(e)))
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
return;
|
|
|
|
}
|
2024-07-23 23:53:34 +01:00
|
|
|
|
2024-11-10 17:08:59 +00:00
|
|
|
tx.send(Ok(format!("{name}@{version_id}"))).await.unwrap();
|
2024-11-10 15:43:25 +00:00
|
|
|
});
|
2024-07-23 15:37:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-10 15:43:25 +00:00
|
|
|
Ok(rx)
|
2024-07-23 15:37:47 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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-11-10 15:43:25 +00:00
|
|
|
|
|
|
|
/// Error interacting with a patched file
|
|
|
|
#[error("error interacting with a patched file")]
|
|
|
|
File(#[source] std::io::Error),
|
2024-07-23 15:37:47 +01:00
|
|
|
}
|
|
|
|
}
|