Return the first changed path whose staged content does not correspond to the current view's recorded content, as `(path, reason)`, or `None` if the candidate tree is coherent with the view (Rule V2, SPEC §6.2). Only paths that differ between the candidate tree and git HEAD are examined (the incremental form), so the check costs one `get_file_content_on_view` per changed path rather than a full-v
(
repo: &Repository,
git_repo: &GitRepository,
candidate_tree_oid: git2::Oid,
view: &str,
)
| 266 | /// changed path rather than a full-view materialize. Provenance / excluded paths |
| 267 | /// are skipped — Rule V4 owns them. |
| 268 | fn first_incoherent_path( |
| 269 | repo: &Repository, |
| 270 | git_repo: &GitRepository, |
| 271 | candidate_tree_oid: git2::Oid, |
| 272 | view: &str, |
| 273 | ) -> CliResult<Option<(String, String)>> { |
| 274 | let candidate_tree = |
| 275 | git_repo |
| 276 | .find_tree(candidate_tree_oid) |
| 277 | .map_err(|e| CliError::GitError { |
| 278 | message: format!("Failed to load candidate tree: {}", e), |
| 279 | })?; |
| 280 | let head_tree = git_repo |
| 281 | .head() |
| 282 | .ok() |
| 283 | .and_then(|h| h.peel_to_commit().ok()) |
| 284 | .and_then(|c| c.tree().ok()); |
| 285 | |
| 286 | let diff = git_repo |
| 287 | .diff_tree_to_tree(head_tree.as_ref(), Some(&candidate_tree), None) |
| 288 | .map_err(|e| CliError::GitError { |
| 289 | message: format!("Failed to diff candidate tree: {}", e), |
| 290 | })?; |
| 291 | |
| 292 | for delta in diff.deltas() { |
| 293 | let (path, in_candidate) = match delta.status() { |
| 294 | git2::Delta::Deleted => match delta.old_file().path().and_then(|p| p.to_str()) { |
| 295 | Some(p) => (p.to_string(), false), |
| 296 | None => continue, |
| 297 | }, |
| 298 | _ => match delta.new_file().path().and_then(|p| p.to_str()) { |
| 299 | Some(p) => (p.to_string(), true), |
| 300 | None => continue, |
| 301 | }, |
| 302 | }; |
| 303 | |
| 304 | // Rule V4 owns provenance / git-excluded paths; V2 ignores them. |
| 305 | if is_forbidden_shadow_path(&path) { |
| 306 | continue; |
| 307 | } |
| 308 | |
| 309 | let view_content = repo |
| 310 | .get_file_content_on_view(&path, view) |
| 311 | .map_err(CliError::Repository)?; |
| 312 | |
| 313 | if in_candidate { |
| 314 | // The staged blob must equal what the view materializes for this path. |
| 315 | let staged = git_repo.find_blob(delta.new_file().id()).ok(); |
| 316 | match ( |
| 317 | staged.as_ref().map(|b| b.content()), |
| 318 | view_content.as_deref(), |
| 319 | ) { |
| 320 | (Some(s), Some(v)) if s == v => {} |
| 321 | (Some(_), Some(_)) => { |
| 322 | return Ok(Some(( |
| 323 | path, |
| 324 | "staged content differs from the view's recorded content".to_string(), |
| 325 | ))); |
no test coverage detected