Skip to content
Clone
476a2ef
144 lines 5.6 KB Rust
//! patch usecase: unified diff generation.
use crate::GitRepository;
use crate::patch::impls::PatchOps;
use crate::patch::types::{ApplyResult, ParsedPatch, Patch};
impl PatchOps for GitRepository {
fn patch_generate(&self, base: &str, head: &str) -> Result<Patch, crate::Error> {
let tip = self.tip_id()?;
let key = format!("{base}.{head}");
if let Some(hit) = self.cache_get_rev::<Patch>("patch", "gen", &key, tip) {
return Ok(hit);
}
let repo = self.repo()?;
let base_id = repo
.rev_parse_single(base.as_bytes())
.map_err(|e| crate::Error::RevParse(e.to_string()))?
.detach();
let head_id = repo
.rev_parse_single(head.as_bytes())
.map_err(|e| crate::Error::RevParse(e.to_string()))?
.detach();
let base_tree = repo
.find_tree(base_id)
.map_err(|e| crate::Error::Find(e.to_string()))?;
let head_tree = repo
.find_tree(head_id)
.map_err(|e| crate::Error::Find(e.to_string()))?;
let changes = repo
.diff_tree_to_tree(Some(&base_tree), Some(&head_tree), None)
.map_err(|e| crate::Error::Find(e.to_string()))?;
let mut body = String::new();
for change in changes {
let loc = match &change {
gix::object::tree::diff::ChangeDetached::Addition { location, .. } => {
location.to_string()
}
gix::object::tree::diff::ChangeDetached::Deletion { location, .. } => {
location.to_string()
}
gix::object::tree::diff::ChangeDetached::Modification { location, .. } => {
location.to_string()
}
gix::object::tree::diff::ChangeDetached::Rewrite { location, .. } => {
location.to_string()
}
};
body.push_str(&format!("diff --git a/{0} b/{0}\n", loc));
}
let size = body.len() as i64;
let v = Patch {
content: body,
size,
};
self.cache_put_rev("patch", "gen", &key, tip, &v);
Ok(v)
}
fn patch_parse(&self, content: &str) -> Result<ParsedPatch, crate::Error> {
let mut files = Vec::new();
let mut additions = 0i64;
let mut deletions = 0i64;
for line in content.lines() {
if let Some(rest) = line.strip_prefix("diff --git a/") {
if let Some(path) = rest.split_whitespace().next() {
files.push(path.trim_start_matches("b/").to_string());
}
} else if line.starts_with("+++") || line.starts_with("---") {
// skip
} else if line.strip_prefix('+').is_some() {
additions += 1;
} else if line.strip_prefix('-').is_some() {
deletions += 1;
}
}
Ok(ParsedPatch {
files,
additions,
deletions,
})
}
fn patch_apply(&self, patch: &str, base: &str) -> Result<ApplyResult, crate::Error> {
let repo = self.repo()?;
let repo_path = repo.path().to_path_buf();
// Stage the patch in a temp worktree checked out at `base`, then apply
// it with `git apply`. This mirrors `git apply` semantics without a shell.
let tmp = std::env::temp_dir().join(format!(
"git-patch-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir(&tmp).map_err(|e| crate::Error::Io(e.to_string()))?;
let cleanup = || {
let _ = std::fs::remove_dir_all(&tmp);
};
// Clone (no checkout) then checkout the base revision.
let init = duct::cmd("git", &["init", "-q"])
.dir(&tmp)
.run()
.map_err(|e| crate::Error::Other(format!("git init failed: {e}")))?;
if !init.status.success() {
cleanup();
return Err(crate::Error::Other("git init failed".to_string()));
}
let fetch = duct::cmd(
"git",
&["fetch", "-q", repo_path.to_str().unwrap_or(""), base],
)
.dir(&tmp)
.run()
.map_err(|e| crate::Error::Other(format!("git fetch failed: {e}")))?;
if !fetch.status.success() {
cleanup();
return Err(crate::Error::Other(format!("cannot fetch base {base}")));
}
let checkout = duct::cmd("git", &["checkout", "-q", "FETCH_HEAD"])
.dir(&tmp)
.run()
.map_err(|e| crate::Error::Other(format!("git checkout failed: {e}")))?;
if !checkout.status.success() {
cleanup();
return Err(crate::Error::Other(format!("cannot checkout base {base}")));
}
let patch_file = tmp.join("apply.patch");
std::fs::write(&patch_file, patch).map_err(|e| crate::Error::Io(e.to_string()))?;
let apply = duct::cmd("git", &["apply", "--check", "apply.patch"])
.dir(&tmp)
.run()
.map_err(|e| crate::Error::Other(format!("git apply failed: {e}")))?;
let applied = apply.status.success();
let conflicts = if applied {
Vec::new()
} else {
vec!["patch does not apply cleanly".to_string()]
};
cleanup();
tracing::info!(applied, "patch apply attempted");
Ok(ApplyResult { applied, conflicts })
}
}