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 { let tip = self.tip_id()?; let key = format!("{base}.{head}"); if let Some(hit) = self.cache_get_rev::("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 { 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("---") { } 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 { let repo = self.repo()?; let repo_path = repo.path().to_path_buf(); 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); }; 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 }) } }