use crate::json_record::{ObjectWriter, decode_fatal, number_equals, parse_object, safe_integer_field, string_field};
use crate::transaction_owner::TransactionOwner;
use crate::private_storage::{create_private_directory, sync_directory, write_exclusive_synced};
use crate::recovery_error::{RecoveryError, io_failure};
use std::path::{Path, PathBuf};
pub const MERGE_INTENT_DIRECTORY: &str = "cli-git-merges";
pub const MERGE_INTENT_SCHEMA_VERSION: i64 = 1;
const RECORD_SUFFIX: &str = ".json";
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MergeIntent {
pub owner: TransactionOwner,
pub repository_root: String,
pub target_ref: Option<String>,
pub old_oid: String,
pub new_oid: String,
}
pub fn intent_directory(git_dir: &Path) -> PathBuf {
return git_dir.join(MERGE_INTENT_DIRECTORY);
}
pub fn intent_path(git_dir: &Path, transaction_id: &str) -> PathBuf {
return intent_directory(git_dir).join(format!("{transaction_id}{RECORD_SUFFIX}"));
}
pub fn encode_merge_intent(intent: &MergeIntent) -> String {
let mut writer: ObjectWriter = ObjectWriter::new();
writer
.integer("schemaVersion", MERGE_INTENT_SCHEMA_VERSION)
.string("state", "merge-intent")
.string("transactionId", intent.owner.transaction_id.as_str())
.integer("ownerPid", intent.owner.owner_pid)
.string("ownerIdentity", intent.owner.owner_identity.as_str())
.string("createdAt", intent.owner.created_at.as_str());
if let Some(namespace) = &intent.owner.owner_pid_namespace {
writer.string("ownerPidNamespace", namespace.as_str());
}
writer
.string("repositoryRoot", intent.repository_root.as_str())
.string("targetRef", intent.target_ref.as_deref().unwrap_or_default())
.string("oldOid", intent.old_oid.as_str())
.string("newOid", intent.new_oid.as_str());
return format!("{}\n", writer.finish());
}
pub fn parse_merge_intent(bytes: &[u8]) -> Option<MergeIntent> {
let map: serde_json::Map<String, serde_json::Value> = parse_object(decode_fatal(bytes)?)?;
if !number_equals(&map, "schemaVersion", MERGE_INTENT_SCHEMA_VERSION)
|| string_field(&map, "state")? != "merge-intent"
{
return None;
}
let owner_pid: i64 = safe_integer_field(&map, "ownerPid")?;
let owner_identity: &str = string_field(&map, "ownerIdentity")?;
let old_oid: &str = string_field(&map, "oldOid")?;
let new_oid: &str = string_field(&map, "newOid")?;
let repository_root: &str = string_field(&map, "repositoryRoot")?;
if owner_pid < 1 || owner_identity.is_empty() || old_oid.is_empty() || new_oid.is_empty()
|| repository_root.is_empty()
{
return None;
}
let target_ref: &str = string_field(&map, "targetRef")?;
return Some(MergeIntent {
owner: TransactionOwner {
transaction_id: String::from(string_field(&map, "transactionId")?),
owner_pid,
owner_identity: String::from(owner_identity),
created_at: String::from(string_field(&map, "createdAt")?),
owner_pid_namespace: string_field(&map, "ownerPidNamespace")
.filter(|namespace| { return !namespace.is_empty(); })
.map(String::from),
},
repository_root: String::from(repository_root),
target_ref: if target_ref.is_empty() { None } else { Some(String::from(target_ref)) },
old_oid: String::from(old_oid),
new_oid: String::from(new_oid),
});
}
pub fn write_merge_intent(git_dir: &Path, intent: &MergeIntent) -> std::io::Result<PathBuf> {
let directory: PathBuf = intent_directory(git_dir);
match create_private_directory(directory.as_path()) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(error),
}
let path: PathBuf = intent_path(git_dir, intent.owner.transaction_id.as_str());
write_exclusive_synced(path.as_path(), encode_merge_intent(intent).as_bytes())?;
sync_directory(directory.as_path())?;
return Ok(path);
}
pub fn remove_merge_intent(path: &Path) -> std::io::Result<()> {
match std::fs::remove_file(path) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(error),
}
if let Some(directory) = path.parent() {
sync_directory(directory)?;
}
return Ok(());
}
pub fn list_merge_intents(git_dir: &Path) -> Result<Vec<PathBuf>, RecoveryError> {
let directory: PathBuf = intent_directory(git_dir);
let listing: std::fs::ReadDir = match std::fs::read_dir(directory.as_path()) {
Ok(found) => found,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => return Err(io_failure("listing", directory.as_path(), &error)),
};
let mut records: Vec<PathBuf> = Vec::new();
for item in listing {
match item {
Ok(entry) => {
if entry.file_name().to_string_lossy().ends_with(RECORD_SUFFIX) {
records.push(entry.path());
}
}
Err(error) => return Err(io_failure("listing", directory.as_path(), &error)),
}
}
records.sort();
return Ok(records);
}
#[cfg(test)]
#[path = "merge_isolation_journal_tests.rs"]
mod tests;