use std::path::Path;
use serde::Serialize;
use void_core::ops::merge_state::{is_merge_in_progress, read_merge_state};
use crate::context::{find_void_dir, void_err_to_cli};
use crate::output::{run_command, CliError, CliOptions};
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConflictsOutput {
pub merge_in_progress: bool,
pub merge_head: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub merge_base: Option<String>,
pub orig_head: String,
pub conflicts: Vec<String>,
pub message: String,
}
pub fn run(cwd: &Path, opts: &CliOptions) -> Result<(), CliError> {
run_command("conflicts", opts, |ctx| {
let void_dir = find_void_dir(cwd)?;
if !is_merge_in_progress(&void_dir) {
return Err(CliError::conflict("no merge in progress"));
}
let merge_state = read_merge_state(&void_dir)
.map_err(void_err_to_cli)?
.ok_or_else(|| CliError::conflict("no merge in progress"))?;
if !ctx.use_json() {
if merge_state.conflicts.is_empty() {
ctx.info("No conflicts. Ready to commit.");
} else {
ctx.info(format!(
"{} conflicted file(s):",
merge_state.conflicts.len()
));
for path in &merge_state.conflicts {
ctx.info(format!(" {}", path));
}
}
}
Ok(ConflictsOutput {
merge_in_progress: true,
merge_head: hex::encode(merge_state.merge_head.as_bytes()),
merge_base: merge_state.merge_base.as_ref().map(|b| hex::encode(b.as_bytes())),
orig_head: hex::encode(merge_state.orig_head.as_bytes()),
conflicts: merge_state.conflicts,
message: merge_state.message,
})
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::output::CliOptions;
use std::fs;
use tempfile::tempdir;
use void_core::crypto::{self, CommitCid};
use void_core::ops::merge_state::{write_merge_state, MergeState};
fn default_opts() -> CliOptions {
CliOptions {
human: true,
..Default::default()
}
}
fn setup_test_repo() -> (tempfile::TempDir, std::path::PathBuf, tempfile::TempDir, crate::context::VoidHomeGuard) {
let dir = tempdir().unwrap();
let void_dir = dir.path().join(".void");
fs::create_dir_all(void_dir.join("objects")).unwrap();
fs::create_dir_all(void_dir.join("refs/heads")).unwrap();
let key = crypto::generate_key();
let home = tempdir().unwrap();
let guard = crate::context::setup_test_manifest(&void_dir, &key, home.path());
fs::write(void_dir.join("config.json"), "{}").unwrap();
(dir, void_dir, home, guard)
}
#[test]
fn test_conflicts_no_merge() {
let (dir, _void_dir, _home, _guard) = setup_test_repo();
let result = run(dir.path(), &default_opts());
assert!(result.is_err());
}
#[test]
fn test_conflicts_with_merge() {
let (dir, void_dir, _home, _guard) = setup_test_repo();
let state = MergeState {
merge_head: CommitCid::from_bytes(vec![0x01, 0x02, 0x03]),
merge_base: None,
orig_head: CommitCid::from_bytes(vec![0x04, 0x05, 0x06]),
conflicts: vec!["src/main.rs".to_string(), "README.md".to_string()],
message: "Merge branch 'feature'".to_string(),
};
write_merge_state(&void_dir, &state).unwrap();
let result = run(dir.path(), &default_opts());
assert!(result.is_ok());
}
#[test]
fn test_conflicts_output_serialization() {
let output = ConflictsOutput {
merge_in_progress: true,
merge_head: "010203".to_string(),
merge_base: None,
orig_head: "040506".to_string(),
conflicts: vec!["src/main.rs".to_string()],
message: "Merge".to_string(),
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"mergeInProgress\":true"));
assert!(json.contains("\"mergeHead\":\"010203\""));
assert!(!json.contains("mergeBase")); assert!(json.contains("\"conflicts\":[\"src/main.rs\"]"));
}
#[test]
fn test_conflicts_output_with_base() {
let output = ConflictsOutput {
merge_in_progress: true,
merge_head: "aabbcc".to_string(),
merge_base: Some("ddeeff".to_string()),
orig_head: "112233".to_string(),
conflicts: vec![],
message: "Merge".to_string(),
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"mergeBase\":\"ddeeff\""));
}
}