use crate::cli::UI;
use crate::ops::refstore;
use anyhow::{bail, Result};
use git2::Repository;
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConflictRecord {
pub trigger_commit: String,
pub operation: String,
pub source_ref: String,
pub timestamp: String,
pub files: Vec<ConflictFileEntry>,
pub resolved: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConflictFileEntry {
pub path: String,
pub base_oid: Option<String>,
pub ours_oid: Option<String>,
pub theirs_oid: Option<String>,
pub resolved: bool,
}
pub fn record_conflicts(
path: &Path,
trigger_commit: &str,
operation: &str,
source_ref: &str,
) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
let files = extract_index_conflicts(&repo)?;
if files.is_empty() {
return Ok(());
}
let record = ConflictRecord {
trigger_commit: trigger_commit.to_string(),
operation: operation.to_string(),
source_ref: source_ref.to_string(),
timestamp: crate::ops::oplog::now_iso8601_pub(),
files,
resolved: false,
};
let ref_name = format!("refs/conflicts/{}", trigger_commit);
refstore::write_json_ref(&repo, &ref_name, &record)?;
Ok(())
}
pub fn list_conflicts(path: &Path, _verbose: bool) -> Result<Vec<ConflictRecord>> {
let repo = crate::ops::open_repo(path)?;
let refs = refstore::list_refs_with_prefix(&repo, "refs/conflicts/")?;
let mut records = Vec::new();
for ref_name in &refs {
if let Some(record) = refstore::read_json_ref::<ConflictRecord>(&repo, ref_name)? {
if !record.resolved {
records.push(record);
}
}
}
Ok(records)
}
pub fn execute_list(path: &Path, verbose: bool, ui: &UI) -> Result<()> {
let records = list_conflicts(path, verbose)?;
if records.is_empty() {
ui.info("No active conflicts");
return Ok(());
}
for record in &records {
ui.section(&format!(
"{} conflict from {} of '{}'",
record.operation, record.timestamp, record.source_ref
));
for file in &record.files {
if file.resolved {
ui.status_item(true, format!("resolved {}", file.path));
} else {
ui.status_item(false, format!("CONFLICT {}", file.path));
}
if verbose && !file.resolved {
if let Some(ref base) = file.base_oid {
ui.field("base", &base[..7.min(base.len())]);
}
if let Some(ref ours) = file.ours_oid {
ui.field("ours", &ours[..7.min(ours.len())]);
}
if let Some(ref theirs) = file.theirs_oid {
ui.field("theirs", &theirs[..7.min(theirs.len())]);
}
}
}
}
Ok(())
}
pub fn resolve_file(path: &Path, file: &str, accept: Option<&str>, ui: &UI) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
if let Some(strategy) = accept {
let index = repo.index()?;
let conflicts: Vec<_> = index.conflicts()?.collect();
let mut resolved_any = false;
for conflict_result in conflicts {
let conflict = conflict_result?;
let conflict_path = conflict
.our
.as_ref()
.or(conflict.their.as_ref())
.and_then(|e| std::str::from_utf8(&e.path).ok())
.unwrap_or("");
if conflict_path == file {
let entry = match strategy {
"ours" => conflict
.our
.ok_or_else(|| anyhow::anyhow!("No 'ours' version for '{}'", file))?,
"theirs" => conflict
.their
.ok_or_else(|| anyhow::anyhow!("No 'theirs' version for '{}'", file))?,
_ => bail!("Unknown strategy '{}'. Use 'ours' or 'theirs'.", strategy),
};
let blob = repo.find_blob(entry.id)?;
let full_path = path.join(file);
std::fs::write(&full_path, blob.content())?;
drop(index);
let mut index = repo.index()?;
index.add_path(Path::new(file))?;
index.write()?;
resolved_any = true;
break;
}
}
if !resolved_any {
bail!("File '{}' is not in a conflicted state.", file);
}
} else {
let mut index = repo.index()?;
index.add_path(Path::new(file))?;
index.write()?;
}
update_conflict_record(&repo, file)?;
ui.success(format!("Resolved: {}", file));
Ok(())
}
pub fn cleanup_resolved(path: &Path) -> Result<()> {
let repo = crate::ops::open_repo(path)?;
let refs = refstore::list_refs_with_prefix(&repo, "refs/conflicts/")?;
for ref_name in &refs {
refstore::delete_ref(&repo, ref_name)?;
}
Ok(())
}
fn extract_index_conflicts(repo: &Repository) -> Result<Vec<ConflictFileEntry>> {
let index = repo.index()?;
let mut files = Vec::new();
for conflict_result in index.conflicts()? {
let conflict = conflict_result?;
let path_str = conflict
.our
.as_ref()
.or(conflict.their.as_ref())
.or(conflict.ancestor.as_ref())
.and_then(|e| std::str::from_utf8(&e.path).ok())
.unwrap_or("")
.to_string();
files.push(ConflictFileEntry {
path: path_str,
base_oid: conflict.ancestor.as_ref().map(|e| e.id.to_string()),
ours_oid: conflict.our.as_ref().map(|e| e.id.to_string()),
theirs_oid: conflict.their.as_ref().map(|e| e.id.to_string()),
resolved: false,
});
}
Ok(files)
}
fn update_conflict_record(repo: &Repository, resolved_file: &str) -> Result<()> {
let refs = refstore::list_refs_with_prefix(repo, "refs/conflicts/")?;
for ref_name in &refs {
if let Some(mut record) = refstore::read_json_ref::<ConflictRecord>(repo, ref_name)? {
let mut changed = false;
for file in &mut record.files {
if file.path == resolved_file && !file.resolved {
file.resolved = true;
changed = true;
}
}
if changed {
record.resolved = record.files.iter().all(|f| f.resolved);
refstore::write_json_ref(repo, ref_name, &record)?;
}
}
}
Ok(())
}