git_policy_cli/
merge_isolation_journal.rs1use super::json_record::{ObjectWriter, decode_fatal, number_equals, parse_object, safe_integer_field, string_field};
18use super::transaction_owner::TransactionOwner;
20use super::private_storage::{create_private_directory, sync_directory, write_exclusive_synced};
22use super::recovery_error::{RecoveryError, io_failure};
24use std::path::{Path, PathBuf};
26
27pub const MERGE_INTENT_DIRECTORY: &str = "cli-git-merges";
29
30pub const MERGE_INTENT_SCHEMA_VERSION: i64 = 1;
32
33const RECORD_SUFFIX: &str = ".json";
35
36#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct MergeIntent {
48 pub owner: TransactionOwner,
50 pub repository_root: String,
52 pub target_ref: Option<String>,
54 pub old_oid: String,
56 pub new_oid: String,
58}
59
60pub fn intent_directory(git_dir: &Path) -> PathBuf {
70 return git_dir.join(MERGE_INTENT_DIRECTORY);
71}
72
73pub fn intent_path(git_dir: &Path, transaction_id: &str) -> PathBuf {
83 return intent_directory(git_dir).join(format!("{transaction_id}{RECORD_SUFFIX}"));
84}
85
86pub fn encode_merge_intent(intent: &MergeIntent) -> String {
96 let mut writer: ObjectWriter = ObjectWriter::new();
97 writer
98 .integer("schemaVersion", MERGE_INTENT_SCHEMA_VERSION)
99 .string("state", "merge-intent")
100 .string("transactionId", intent.owner.transaction_id.as_str())
101 .integer("ownerPid", intent.owner.owner_pid)
102 .string("ownerIdentity", intent.owner.owner_identity.as_str())
103 .string("createdAt", intent.owner.created_at.as_str());
104 if let Some(namespace) = &intent.owner.owner_pid_namespace {
105 writer.string("ownerPidNamespace", namespace.as_str());
106 }
107 writer
108 .string("repositoryRoot", intent.repository_root.as_str())
109 .string("targetRef", intent.target_ref.as_deref().unwrap_or_default())
110 .string("oldOid", intent.old_oid.as_str())
111 .string("newOid", intent.new_oid.as_str());
112 return format!("{}\n", writer.finish());
113}
114
115pub fn parse_merge_intent(bytes: &[u8]) -> Option<MergeIntent> {
125 let map: serde_json::Map<String, serde_json::Value> = parse_object(decode_fatal(bytes)?)?;
126 if !number_equals(&map, "schemaVersion", MERGE_INTENT_SCHEMA_VERSION)
127 || string_field(&map, "state")? != "merge-intent"
128 {
129 return None;
130 }
131 let owner_pid: i64 = safe_integer_field(&map, "ownerPid")?;
132 let owner_identity: &str = string_field(&map, "ownerIdentity")?;
133 let old_oid: &str = string_field(&map, "oldOid")?;
134 let new_oid: &str = string_field(&map, "newOid")?;
135 let repository_root: &str = string_field(&map, "repositoryRoot")?;
136 if owner_pid < 1 || owner_identity.is_empty() || old_oid.is_empty() || new_oid.is_empty()
137 || repository_root.is_empty()
138 {
139 return None;
140 }
141 let target_ref: &str = string_field(&map, "targetRef")?;
142 return Some(MergeIntent {
143 owner: TransactionOwner {
144 transaction_id: String::from(string_field(&map, "transactionId")?),
145 owner_pid,
146 owner_identity: String::from(owner_identity),
147 created_at: String::from(string_field(&map, "createdAt")?),
148 owner_pid_namespace: string_field(&map, "ownerPidNamespace")
149 .filter(|namespace| { return !namespace.is_empty(); })
150 .map(String::from),
151 },
152 repository_root: String::from(repository_root),
153 target_ref: if target_ref.is_empty() { None } else { Some(String::from(target_ref)) },
154 old_oid: String::from(old_oid),
155 new_oid: String::from(new_oid),
156 });
157}
158
159pub fn write_merge_intent(git_dir: &Path, intent: &MergeIntent) -> std::io::Result<PathBuf> {
169 let directory: PathBuf = intent_directory(git_dir);
170 match create_private_directory(directory.as_path()) {
171 Ok(()) => {}
172 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
173 Err(error) => return Err(error),
174 }
175 let path: PathBuf = intent_path(git_dir, intent.owner.transaction_id.as_str());
176 write_exclusive_synced(path.as_path(), encode_merge_intent(intent).as_bytes())?;
177 sync_directory(directory.as_path())?;
178 return Ok(path);
179}
180
181pub fn remove_merge_intent(path: &Path) -> std::io::Result<()> {
191 match std::fs::remove_file(path) {
192 Ok(()) => {}
193 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
194 Err(error) => return Err(error),
195 }
196 if let Some(directory) = path.parent() {
197 sync_directory(directory)?;
198 }
199 return Ok(());
200}
201
202pub fn list_merge_intents(git_dir: &Path) -> Result<Vec<PathBuf>, RecoveryError> {
212 let directory: PathBuf = intent_directory(git_dir);
213 let listing: std::fs::ReadDir = match std::fs::read_dir(directory.as_path()) {
214 Ok(found) => found,
215 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
216 Err(error) => return Err(io_failure("listing", directory.as_path(), &error)),
217 };
218 let mut records: Vec<PathBuf> = Vec::new();
219 for item in listing {
220 match item {
221 Ok(entry) => {
222 if entry.file_name().to_string_lossy().ends_with(RECORD_SUFFIX) {
223 records.push(entry.path());
224 }
225 }
226 Err(error) => return Err(io_failure("listing", directory.as_path(), &error)),
227 }
228 }
229 records.sort();
230 return Ok(records);
231}
232
233#[cfg(test)]
235#[path = "merge_isolation_journal_tests.rs"]
236mod tests;