Add local file conflict metadata

This commit is contained in:
Eric Wendland 2026-05-18 03:57:26 +02:00
commit 20c88af800
13 changed files with 673 additions and 15 deletions

View file

@ -1,7 +1,10 @@
pub mod service;
use geth_auth::{AuthExplanation, AuthOp, AuthOpKind};
use geth_cas::{BlobInfoSummary, FileRoot, FileRootScan, LocalCas, hash_path};
use geth_cas::{
BlobInfoSummary, FileConflict, FileConflictKind, FileConflictResolution, FileConflictStatus,
FileRoot, FileRootScan, LocalCas, hash_path,
};
use geth_config::{GethConfig, GethPaths, RelayMode};
use geth_control::{
CasBlob, ControlRequest, ControlResponse, KeychainStatusResponse, NodeIdResponse,
@ -23,9 +26,9 @@ use geth_ssh_identity::{
cert_request_id, certificate_id, openssh_krl_spec, revocation_id, ssh_public_key_fingerprint,
};
use geth_store::{
Store, StoredAuthOp, StoredDbResource, StoredDocumentResource, StoredFileRoot,
StoredKeychainOp, StoredKvEntry, StoredKvStore, StoredResource, StoredResourceSecret,
StoredSshCertRequest, StoredSshCertificate, StoredSshRevocation,
Store, StoredAuthOp, StoredDbResource, StoredDocumentResource, StoredFileConflict,
StoredFileRoot, StoredKeychainOp, StoredKvEntry, StoredKvStore, StoredResource,
StoredResourceSecret, StoredSshCertRequest, StoredSshCertificate, StoredSshRevocation,
};
use geth_types::{
AuthOpId, Capability, KeyId, NodeId, PrincipalId, ResourceId, ResourceKind, ResourceName,
@ -414,6 +417,81 @@ pub fn handle_request(
},
})
}
ControlRequest::CasConflictRecord {
root,
path,
kind,
detail,
base_tree,
local_tree,
remote_tree,
} => {
geth_cas::validate_file_root_name(&root)?;
if let Some(hash) = &base_tree {
geth_cas::validate_hash(hash)?;
}
if let Some(hash) = &local_tree {
geth_cas::validate_hash(hash)?;
}
if let Some(hash) = &remote_tree {
geth_cas::validate_hash(hash)?;
}
let root_record = store
.get_file_root_by_name(&root)?
.ok_or_else(|| NodeError::ResourceNotFound(format!("file-root:{root}")))?;
let kind = FileConflictKind::parse(&kind)?;
let created_at = geth_store::now_ms();
let conflict_id = generated_file_conflict_id(&root, &path, kind.as_str(), created_at);
let conflict = StoredFileConflict {
conflict_id,
root_name: root,
resource_id: root_record.resource_id,
path,
kind: kind.as_str().to_owned(),
status: FileConflictStatus::Open.as_str().to_owned(),
base_tree_hash: base_tree.map(|hash| hash.to_string()),
local_tree_hash: local_tree.map(|hash| hash.to_string()),
remote_tree_hash: remote_tree.map(|hash| hash.to_string()),
detail,
resolution: None,
resolution_note: None,
created_at_ms: created_at,
resolved_at_ms: None,
};
store.upsert_file_conflict(&conflict)?;
Ok(ControlResponse::CasConflictRecorded {
conflict: file_conflict_from_stored(conflict)?,
})
}
ControlRequest::CasConflictList { root } => {
if let Some(root) = root.as_deref() {
geth_cas::validate_file_root_name(root)?;
}
let conflicts = store
.list_file_conflicts(root.as_deref())?
.into_iter()
.map(file_conflict_from_stored)
.collect::<Result<Vec<_>, _>>()?;
Ok(ControlResponse::CasConflictList { conflicts })
}
ControlRequest::CasConflictResolve {
conflict_id,
resolution,
note,
} => {
let resolution = FileConflictResolution::parse(&resolution)?;
let mut conflict = store
.get_file_conflict(&conflict_id)?
.ok_or_else(|| NodeError::ResourceNotFound(conflict_id.clone()))?;
conflict.status = FileConflictStatus::Resolved.as_str().to_owned();
conflict.resolution = Some(resolution.as_str().to_owned());
conflict.resolution_note = note;
conflict.resolved_at_ms = Some(geth_store::now_ms());
store.upsert_file_conflict(&conflict)?;
Ok(ControlResponse::CasConflictResolved {
conflict: file_conflict_from_stored(conflict)?,
})
}
ControlRequest::KeychainInit { admin_key_path } => {
let mut ops = Vec::new();
let created_at = UnixMillis(geth_store::now_ms());
@ -1136,6 +1214,29 @@ fn file_root_from_stored(stored: &StoredFileRoot) -> FileRoot {
}
}
fn file_conflict_from_stored(stored: StoredFileConflict) -> Result<FileConflict, NodeError> {
Ok(FileConflict {
id: stored.conflict_id,
root: stored.root_name,
resource: stored.resource_id,
path: stored.path,
kind: FileConflictKind::parse(&stored.kind)?,
status: FileConflictStatus::parse(&stored.status)?,
base_tree: stored.base_tree_hash.map(Into::into),
local_tree: stored.local_tree_hash.map(Into::into),
remote_tree: stored.remote_tree_hash.map(Into::into),
detail: stored.detail,
resolution: stored
.resolution
.as_deref()
.map(FileConflictResolution::parse)
.transpose()?,
resolution_note: stored.resolution_note,
created_at_ms: stored.created_at_ms,
resolved_at_ms: stored.resolved_at_ms,
})
}
fn ensure_resource_exists(store: &Store, resource_id: &str) -> Result<(), NodeError> {
if store
.list_resources()?
@ -1240,6 +1341,13 @@ fn generated_grant_id(subject: &str, resource: &str, capability: &str) -> String
)
}
fn generated_file_conflict_id(root: &str, path: &str, kind: &str, created_at_ms: i64) -> String {
format!(
"file-conflict:{}",
geth_crypto::blake3_hex(format!("{created_at_ms}\0{root}\0{path}\0{kind}").as_bytes())
)
}
fn generated_auth_op_id(
kind: &str,
resource: &str,