// file: crates/knowledge-git-api/src/write.rs // version: 1 /// Invalid value supplied to the provider-neutral Git write contract. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum GitWriteInputError { /// A mutation path is empty or contains only whitespace. EmptyMutationPath, /// A mutation path is absolute or uses a platform-specific separator. AbsoluteMutationPath, /// A mutation path contains an empty, current-directory or parent-directory component. UnnormalizedMutationPath, /// A mutation path addresses Git's private `.git` area. ProtectedGitPath, /// A commit identity name is empty or contains a forbidden line break or NUL byte. InvalidCommitIdentityName, /// A commit identity email is empty or contains a forbidden line break or NUL byte. InvalidCommitIdentityEmail, /// A commit message is empty or contains a NUL byte. InvalidCommitMessage, /// A write request does not contain any file mutation. NoMutations, /// More than one mutation targets the same normalized path. DuplicateMutationPath, /// A target branch is not a valid full `refs/heads/...` Git reference. InvalidBranch, } impl std::fmt::Display for crate::GitWriteInputError { fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { crate::GitWriteInputError::EmptyMutationPath => return formatter.write_str("Git mutation path must not be empty"), crate::GitWriteInputError::AbsoluteMutationPath => return formatter.write_str("Git mutation path must be relative and use '/' separators"), crate::GitWriteInputError::UnnormalizedMutationPath => return formatter.write_str("Git mutation path must be normalized"), crate::GitWriteInputError::ProtectedGitPath => return formatter.write_str("Git mutation path must not address .git"), crate::GitWriteInputError::InvalidCommitIdentityName => return formatter.write_str("Git commit identity name is invalid"), crate::GitWriteInputError::InvalidCommitIdentityEmail => return formatter.write_str("Git commit identity email is invalid"), crate::GitWriteInputError::InvalidCommitMessage => return formatter.write_str("Git commit message is invalid"), crate::GitWriteInputError::NoMutations => return formatter.write_str("Git write request must contain at least one mutation"), crate::GitWriteInputError::DuplicateMutationPath => return formatter.write_str("Git write request must not mutate one path more than once"), crate::GitWriteInputError::InvalidBranch => return formatter.write_str("Git target branch must be a valid refs/heads reference"), } } } impl std::error::Error for crate::GitWriteInputError {} /// Normalized relative path that may be mutated inside an isolated Git write worktree. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct GitMutationPath(String); impl crate::GitMutationPath { /// Validate and build a provider-neutral repository-relative mutation path. pub fn new(value: &str) -> Result { if value.trim().is_empty() { return Err(crate::GitWriteInputError::EmptyMutationPath); } if value.starts_with('/') || value.contains('\\') || crate::write::has_windows_drive_prefix(value) { return Err(crate::GitWriteInputError::AbsoluteMutationPath); } if value.contains('\0') { return Err(crate::GitWriteInputError::UnnormalizedMutationPath); } for component in value.split('/') { if component.is_empty() || component == "." || component == ".." { return Err(crate::GitWriteInputError::UnnormalizedMutationPath); } if component.eq_ignore_ascii_case(".git") { return Err(crate::GitWriteInputError::ProtectedGitPath); } } return Ok(Self(value.to_owned())); } /// Return the normalized repository-relative path. pub fn as_str(&self) -> &str { return self.0.as_str(); } } /// One file mutation requested for a candidate Git commit. #[derive(Clone, Debug, Eq, PartialEq)] pub enum GitFileMutation { /// Create or replace one file with the provided bytes. Write { /// Validated repository-relative file path. path: crate::GitMutationPath, /// Complete bytes that must become the file content. content: Vec, }, /// Delete one existing path from the candidate tree. Delete { /// Validated repository-relative file path. path: crate::GitMutationPath, }, } impl crate::GitFileMutation { /// Return the validated path targeted by this mutation. pub fn path(&self) -> &crate::GitMutationPath { match self { crate::GitFileMutation::Write { path, .. } => return path, crate::GitFileMutation::Delete { path } => return path, } } } /// Explicit author and committer identity used to create a candidate commit. #[derive(Clone, Debug, Eq, PartialEq)] pub struct GitCommitIdentity { name: String, email: String, } impl crate::GitCommitIdentity { /// Validate and build an explicit Git commit identity. pub fn new(name: &str, email: &str) -> Result { if crate::write::invalid_identity_component(name) { return Err(crate::GitWriteInputError::InvalidCommitIdentityName); } if crate::write::invalid_identity_component(email) { return Err(crate::GitWriteInputError::InvalidCommitIdentityEmail); } return Ok(Self { name: name.to_owned(), email: email.to_owned() }); } /// Return the explicit identity name. pub fn name(&self) -> &str { return self.name.as_str(); } /// Return the explicit identity email. pub fn email(&self) -> &str { return self.email.as_str(); } } /// Validated full Git branch reference accepted as a remote write target. #[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct GitBranch(String); impl crate::GitBranch { /// Validate a full `refs/heads/...` branch reference. pub fn new(value: &str) -> Result { if !crate::write::valid_branch_reference(value) { return Err(crate::GitWriteInputError::InvalidBranch); } return Ok(Self(value.to_owned())); } /// Return the validated full branch reference. pub fn as_str(&self) -> &str { return self.0.as_str(); } } /// Expected or observed state of one remote branch. #[derive(Clone, Debug, Eq, PartialEq)] pub enum GitRemoteState { /// The branch does not exist. Absent, /// The branch resolves to the provided revision. Revision(crate::GitRevision), } /// Request to build one candidate commit from an explicit base revision. #[derive(Clone, Debug, Eq, PartialEq)] pub struct GitWriteRequest { base_revision: crate::GitRevision, mutations: Vec, identity: crate::GitCommitIdentity, message: String, } impl crate::GitWriteRequest { /// Validate and build a candidate-commit request. pub fn new( base_revision: crate::GitRevision, mutations: Vec, identity: crate::GitCommitIdentity, message: &str, ) -> Result { if mutations.is_empty() { return Err(crate::GitWriteInputError::NoMutations); } if message.trim().is_empty() || message.contains('\0') { return Err(crate::GitWriteInputError::InvalidCommitMessage); } let mut paths = std::collections::HashSet::new(); for mutation in &mutations { if !paths.insert(mutation.path().as_str()) { return Err(crate::GitWriteInputError::DuplicateMutationPath); } } return Ok(Self { base_revision, mutations, identity, message: message.to_owned() }); } /// Return the resolved base revision from which the candidate must be created. pub fn base_revision(&self) -> &crate::GitRevision { return &self.base_revision; } /// Return the ordered mutations to apply in the isolated write worktree. pub fn mutations(&self) -> &[crate::GitFileMutation] { return self.mutations.as_slice(); } /// Return the explicit author/committer identity. pub fn identity(&self) -> &crate::GitCommitIdentity { return &self.identity; } /// Return the candidate commit message. pub fn message(&self) -> &str { return self.message.as_str(); } } /// Request to compare-and-exchange one remote branch to a candidate revision. #[derive(Clone, Debug, Eq, PartialEq)] pub struct GitPushRequest { target_branch: crate::GitBranch, expected_remote: crate::GitRemoteState, candidate_revision: crate::GitRevision, } impl crate::GitPushRequest { /// Build a guarded remote update request from already validated values. pub fn new(target_branch: crate::GitBranch, expected_remote: crate::GitRemoteState, candidate_revision: crate::GitRevision) -> crate::GitPushRequest { return Self { target_branch, expected_remote, candidate_revision }; } /// Return the target remote branch. pub fn target_branch(&self) -> &crate::GitBranch { return &self.target_branch; } /// Return the remote state that must still hold for the update to succeed. pub fn expected_remote(&self) -> &crate::GitRemoteState { return &self.expected_remote; } /// Return the candidate revision to publish. pub fn candidate_revision(&self) -> &crate::GitRevision { return &self.candidate_revision; } } /// Result of one guarded remote branch update. #[derive(Clone, Debug, Eq, PartialEq)] pub enum GitPushOutcome { /// The target branch was updated to the candidate revision. Updated( /// Revision now expected at the updated target branch. crate::GitRevision, ), /// The target branch no longer matched the expected state. Conflict { /// Actual remote state observed when the guarded update was rejected. actual: crate::GitRemoteState, }, } /// Provider-neutral write operations required from a Git repository backend. /// /// This capability is intentionally separate from [`crate::GitRepository`] so read-only consumers /// and fakes do not need to implement write methods. A concrete backend may implement this trait /// independently from the read contract. pub trait GitRepositoryWriter { /// Implementation-specific recoverable error. type Error; /// Create a candidate commit without updating a remote branch. fn create_candidate(&mut self, source_id: &knowledge_domain_lib::SourceId, request: &crate::GitWriteRequest) -> Result; /// Attempt one guarded remote branch update for a previously created candidate revision. fn push_candidate(&mut self, source_id: &knowledge_domain_lib::SourceId, request: &crate::GitPushRequest) -> Result; } fn has_windows_drive_prefix(value: &str) -> bool { let bytes = value.as_bytes(); if bytes.len() < 2 { return false; } return bytes[0].is_ascii_alphabetic() && bytes[1] == b':'; } fn invalid_identity_component(value: &str) -> bool { return value.trim().is_empty() || value.contains('\0') || value.contains('\n') || value.contains('\r'); } fn valid_branch_reference(value: &str) -> bool { const PREFIX: &str = "refs/heads/"; let branch = match value.strip_prefix(PREFIX) { Some(value) => value, None => return false, }; if branch.is_empty() || branch.starts_with('/') || branch.ends_with('/') || branch.ends_with('.') || branch.contains("..") || branch.contains("@{") || branch.contains("//") { return false; } for byte in branch.bytes() { if byte <= 0x20 || byte == 0x7f || matches!(byte, b'~' | b'^' | b':' | b'?' | b'*' | b'[' | b'\\') { return false; } } for component in branch.split('/') { if component.is_empty() || component.starts_with('.') || component.ends_with(".lock") { return false; } } return true; }