0.0.3-alpha.4
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/knowledge-git-lib/src/error.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Recoverable errors for controlled Git process execution.
|
||||
|
||||
@@ -30,6 +30,10 @@ pub enum GitOperation {
|
||||
PreserveCandidate,
|
||||
/// Remove one backend-owned candidate worktree.
|
||||
RemoveWriteWorktree,
|
||||
/// Observe the current state of one remote branch.
|
||||
InspectRemote,
|
||||
/// Push one candidate revision with an explicit compare-and-exchange guard.
|
||||
PushCandidate,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for crate::GitOperation {
|
||||
@@ -47,6 +51,8 @@ impl std::fmt::Display for crate::GitOperation {
|
||||
crate::GitOperation::CommitCandidate => return formatter.write_str("commit candidate"),
|
||||
crate::GitOperation::PreserveCandidate => return formatter.write_str("preserve candidate"),
|
||||
crate::GitOperation::RemoveWriteWorktree => return formatter.write_str("remove write worktree"),
|
||||
crate::GitOperation::InspectRemote => return formatter.write_str("inspect remote branch"),
|
||||
crate::GitOperation::PushCandidate => return formatter.write_str("push guarded candidate"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// file: crates/knowledge-git-lib/src/lib.rs
|
||||
// version: 4
|
||||
// version: 5
|
||||
|
||||
//! Controlled Git process backend for Knowledge content sources.
|
||||
//!
|
||||
//! This crate owns physical Git source configuration, generated-state layout, provider-neutral read
|
||||
//! materialization, isolated candidate construction and process execution. It deliberately contains no
|
||||
//! forge API, persistence, scheduler or application orchestration.
|
||||
//! materialization, isolated candidate construction, guarded remote branch updates and process execution.
|
||||
//! It deliberately contains no forge API, persistence, scheduler or application orchestration.
|
||||
|
||||
mod backend;
|
||||
mod error;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// file: crates/knowledge-git-lib/src/write.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Isolated candidate-commit creation for controlled Git sources.
|
||||
//! Isolated candidate-commit creation and guarded remote publication for controlled Git sources.
|
||||
|
||||
impl crate::GitBackend {
|
||||
/// Create one detached candidate commit from an explicit resolved base revision.
|
||||
@@ -223,6 +223,189 @@ impl crate::GitBackend {
|
||||
Err(_) => return Err(crate::GitBackendError::WriteCleanupFailed),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt one guarded remote branch update for a retained candidate revision.
|
||||
///
|
||||
/// The remote is first observed for an early conflict result, then Git's server-side
|
||||
/// `--force-with-lease=<ref>:<expected>` guard performs the authoritative compare-and-exchange.
|
||||
/// A failed guarded push is observed again so a concurrent update is returned as a recoverable
|
||||
/// [`knowledge_git_api::GitPushOutcome::Conflict`] instead of being collapsed into a process error.
|
||||
pub fn push_candidate(
|
||||
&mut self,
|
||||
source_id: &knowledge_domain_lib::SourceId,
|
||||
request: &knowledge_git_api::GitPushRequest,
|
||||
) -> Result<knowledge_git_api::GitPushOutcome, crate::GitBackendError> {
|
||||
if !crate::is_safe_revision(request.candidate_revision().as_str()) {
|
||||
return Err(crate::GitBackendError::InvalidRevision);
|
||||
}
|
||||
let source_result = self.configured_source(source_id);
|
||||
let source = match source_result {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let mirror_result = self.ensure_mirror(source);
|
||||
let mirror = match mirror_result {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let candidate_result = self.validate_retained_candidate(mirror.repository_path(), request.candidate_revision());
|
||||
match candidate_result {
|
||||
Ok(()) => (),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
let observed_result = self.observe_remote_state(mirror.repository_path(), request.target_branch());
|
||||
let observed = match observed_result {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if &observed != request.expected_remote() {
|
||||
return Ok(knowledge_git_api::GitPushOutcome::Conflict { actual: observed });
|
||||
}
|
||||
let lease = match request.expected_remote() {
|
||||
knowledge_git_api::GitRemoteState::Absent => format!("--force-with-lease={}:", request.target_branch().as_str()),
|
||||
knowledge_git_api::GitRemoteState::Revision(revision) => {
|
||||
if !crate::is_safe_revision(revision.as_str()) {
|
||||
return Err(crate::GitBackendError::InvalidRevision);
|
||||
}
|
||||
format!("--force-with-lease={}:{}", request.target_branch().as_str(), revision.as_str())
|
||||
},
|
||||
};
|
||||
let refspec = format!("{}:{}", request.candidate_revision().as_str(), request.target_branch().as_str());
|
||||
let push_result = self.run_controlled_git(
|
||||
crate::GitOperation::PushCandidate,
|
||||
vec![
|
||||
std::ffi::OsString::from("--git-dir"),
|
||||
mirror.repository_path().as_os_str().to_os_string(),
|
||||
std::ffi::OsString::from("-c"),
|
||||
std::ffi::OsString::from("remote.origin.mirror=false"),
|
||||
std::ffi::OsString::from("push"),
|
||||
std::ffi::OsString::from("--porcelain"),
|
||||
std::ffi::OsString::from("--no-verify"),
|
||||
std::ffi::OsString::from(lease),
|
||||
std::ffi::OsString::from("origin"),
|
||||
std::ffi::OsString::from(refspec),
|
||||
],
|
||||
Vec::new(),
|
||||
);
|
||||
match push_result {
|
||||
Ok(_) => return Ok(knowledge_git_api::GitPushOutcome::Updated(request.candidate_revision().clone())),
|
||||
Err(push_error) => {
|
||||
let after_result = self.observe_remote_state(mirror.repository_path(), request.target_branch());
|
||||
let after = match after_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Err(push_error),
|
||||
};
|
||||
if &after != request.expected_remote() {
|
||||
if after == knowledge_git_api::GitRemoteState::Revision(request.candidate_revision().clone()) {
|
||||
return Ok(knowledge_git_api::GitPushOutcome::Updated(request.candidate_revision().clone()));
|
||||
}
|
||||
return Ok(knowledge_git_api::GitPushOutcome::Conflict { actual: after });
|
||||
}
|
||||
return Err(push_error);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_retained_candidate(&self, repository_path: &std::path::Path, candidate: &knowledge_git_api::GitRevision) -> Result<(), crate::GitBackendError> {
|
||||
let candidate_reference = format!("refs/knowledge/candidates/{}^{{commit}}", candidate.as_str());
|
||||
let output_result = self.run_controlled_git(
|
||||
crate::GitOperation::InspectCandidate,
|
||||
vec![
|
||||
std::ffi::OsString::from("--git-dir"),
|
||||
repository_path.as_os_str().to_os_string(),
|
||||
std::ffi::OsString::from("rev-parse"),
|
||||
std::ffi::OsString::from("--verify"),
|
||||
std::ffi::OsString::from("--end-of-options"),
|
||||
std::ffi::OsString::from(candidate_reference),
|
||||
],
|
||||
Vec::new(),
|
||||
);
|
||||
let output = match output_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Err(crate::GitBackendError::InvalidCandidate),
|
||||
};
|
||||
let text_result = std::str::from_utf8(&output);
|
||||
let text = match text_result {
|
||||
Ok(value) => value.trim(),
|
||||
Err(_) => return Err(crate::GitBackendError::InvalidCandidate),
|
||||
};
|
||||
if text != candidate.as_str() {
|
||||
return Err(crate::GitBackendError::InvalidCandidate);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn observe_remote_state(
|
||||
&self,
|
||||
repository_path: &std::path::Path,
|
||||
branch: &knowledge_git_api::GitBranch,
|
||||
) -> Result<knowledge_git_api::GitRemoteState, crate::GitBackendError> {
|
||||
let output_result = self.run_controlled_git(
|
||||
crate::GitOperation::InspectRemote,
|
||||
vec![
|
||||
std::ffi::OsString::from("--git-dir"),
|
||||
repository_path.as_os_str().to_os_string(),
|
||||
std::ffi::OsString::from("ls-remote"),
|
||||
std::ffi::OsString::from("--refs"),
|
||||
std::ffi::OsString::from("origin"),
|
||||
std::ffi::OsString::from(branch.as_str()),
|
||||
],
|
||||
Vec::new(),
|
||||
);
|
||||
let output = match output_result {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if output.is_empty() {
|
||||
return Ok(knowledge_git_api::GitRemoteState::Absent);
|
||||
}
|
||||
let text_result = std::str::from_utf8(&output);
|
||||
let text = match text_result {
|
||||
Ok(value) => value.trim(),
|
||||
Err(_) => return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::InspectRemote }),
|
||||
};
|
||||
let mut lines = text.lines();
|
||||
let line = match lines.next() {
|
||||
Some(value) => value,
|
||||
None => return Ok(knowledge_git_api::GitRemoteState::Absent),
|
||||
};
|
||||
if lines.next().is_some() {
|
||||
return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::InspectRemote });
|
||||
}
|
||||
let mut fields = line.split('\t');
|
||||
let revision = match fields.next() {
|
||||
Some(value) => value,
|
||||
None => return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::InspectRemote }),
|
||||
};
|
||||
let remote_reference = match fields.next() {
|
||||
Some(value) => value,
|
||||
None => return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::InspectRemote }),
|
||||
};
|
||||
if fields.next().is_some() || remote_reference != branch.as_str() || !crate::is_safe_revision(revision) {
|
||||
return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::InspectRemote });
|
||||
}
|
||||
return Ok(knowledge_git_api::GitRemoteState::Revision(knowledge_git_api::GitRevision::new(revision)));
|
||||
}
|
||||
}
|
||||
|
||||
impl knowledge_git_api::GitRepositoryWriter for crate::GitBackend {
|
||||
type Error = crate::GitBackendError;
|
||||
|
||||
fn create_candidate(
|
||||
&mut self,
|
||||
source_id: &knowledge_domain_lib::SourceId,
|
||||
request: &knowledge_git_api::GitWriteRequest,
|
||||
) -> Result<knowledge_git_api::GitRevision, Self::Error> {
|
||||
return crate::GitBackend::create_candidate(self, source_id, request);
|
||||
}
|
||||
|
||||
fn push_candidate(
|
||||
&mut self,
|
||||
source_id: &knowledge_domain_lib::SourceId,
|
||||
request: &knowledge_git_api::GitPushRequest,
|
||||
) -> Result<knowledge_git_api::GitPushOutcome, Self::Error> {
|
||||
return crate::GitBackend::push_candidate(self, source_id, request);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_mutation(worktree_path: &std::path::Path, mutation: &knowledge_git_api::GitFileMutation) -> Result<(), crate::GitBackendError> {
|
||||
|
||||
282
crates/knowledge-git-lib/tests/git_write_push.rs
Normal file
282
crates/knowledge-git-lib/tests/git_write_push.rs
Normal file
@@ -0,0 +1,282 @@
|
||||
// file: crates/knowledge-git-lib/tests/git_write_push.rs
|
||||
// version: 1
|
||||
|
||||
//! Integration tests for guarded real Git pushes using local bare remotes.
|
||||
|
||||
fn test_root(label: &str) -> std::path::PathBuf {
|
||||
let nanos_result = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
|
||||
let nanos = match nanos_result {
|
||||
Ok(value) => value.as_nanos(),
|
||||
Err(_) => 0,
|
||||
};
|
||||
let mut path = std::env::temp_dir();
|
||||
path.push(format!("sasedev-knowledge-git-push-{label}-{}-{nanos}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&path);
|
||||
return path;
|
||||
}
|
||||
|
||||
fn run_git(repository: &std::path::Path, arguments: &[&str]) -> bool {
|
||||
let mut command = std::process::Command::new("git");
|
||||
command.current_dir(repository);
|
||||
command.args(arguments);
|
||||
command.env("GIT_TERMINAL_PROMPT", "0");
|
||||
let status_result = command.status();
|
||||
match status_result {
|
||||
Ok(status) => return status.success(),
|
||||
Err(_) => return false,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_git_with_path(repository: &std::path::Path, arguments: &[&str], path: &std::path::Path) -> bool {
|
||||
let mut command = std::process::Command::new("git");
|
||||
command.current_dir(repository);
|
||||
command.args(arguments);
|
||||
command.arg(path);
|
||||
command.env("GIT_TERMINAL_PROMPT", "0");
|
||||
let status_result = command.status();
|
||||
match status_result {
|
||||
Ok(status) => return status.success(),
|
||||
Err(_) => return false,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_git_dir_output(repository: &std::path::Path, reference: &str) -> Option<String> {
|
||||
let mut command = std::process::Command::new("git");
|
||||
command.arg("--git-dir");
|
||||
command.arg(repository);
|
||||
command.arg("rev-parse");
|
||||
command.arg("--verify");
|
||||
command.arg(reference);
|
||||
command.env("GIT_TERMINAL_PROMPT", "0");
|
||||
let output_result = command.output();
|
||||
let output = match output_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return None,
|
||||
};
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
let text_result = std::str::from_utf8(&output.stdout);
|
||||
match text_result {
|
||||
Ok(value) => return Some(value.trim().to_owned()),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_remote(root: &std::path::Path) -> Option<(std::path::PathBuf, std::path::PathBuf)> {
|
||||
let remote = root.join("remote.git");
|
||||
let seed = root.join("seed");
|
||||
if std::fs::create_dir_all(root).is_err() {
|
||||
return None;
|
||||
}
|
||||
if !run_git_with_path(root, &["init", "--bare"], &remote) {
|
||||
return None;
|
||||
}
|
||||
if !run_git_with_path(root, &["init", "-b", "main"], &seed) {
|
||||
return None;
|
||||
}
|
||||
if !run_git(&seed, &["config", "user.name", "Remote Writer"]) {
|
||||
return None;
|
||||
}
|
||||
if !run_git(&seed, &["config", "user.email", "remote@example.invalid"]) {
|
||||
return None;
|
||||
}
|
||||
if !run_git(&seed, &["config", "commit.gpgsign", "false"]) {
|
||||
return None;
|
||||
}
|
||||
if std::fs::write(seed.join("content.txt"), "first\n").is_err() {
|
||||
return None;
|
||||
}
|
||||
if !run_git(&seed, &["add", "content.txt"]) {
|
||||
return None;
|
||||
}
|
||||
if !run_git(&seed, &["commit", "-m", "first"]) {
|
||||
return None;
|
||||
}
|
||||
if !run_git_with_path(&seed, &["remote", "add", "origin"], &remote) {
|
||||
return None;
|
||||
}
|
||||
if !run_git(&seed, &["push", "origin", "main"]) {
|
||||
return None;
|
||||
}
|
||||
return Some((remote, seed));
|
||||
}
|
||||
|
||||
fn configured_backend(root: &std::path::Path, remote: &std::path::Path) -> Option<(knowledge_git_lib::GitBackend, knowledge_domain_lib::SourceId)> {
|
||||
let source_id_result = knowledge_domain_lib::SourceId::new("integration-push-source");
|
||||
let source_id = match source_id_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let config_result = knowledge_git_lib::GitBackendConfig::new(root.join("state"));
|
||||
let config = match config_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let source_result = knowledge_git_lib::GitSourceConfig::local(source_id.clone(), remote.to_path_buf());
|
||||
let source_config = match source_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let mut backend = knowledge_git_lib::GitBackend::new(config);
|
||||
if backend.register_source(source_config).is_err() {
|
||||
return None;
|
||||
}
|
||||
return Some((backend, source_id));
|
||||
}
|
||||
|
||||
fn resolved_main(backend: &mut knowledge_git_lib::GitBackend, source_id: &knowledge_domain_lib::SourceId) -> Option<knowledge_git_api::GitRevision> {
|
||||
if knowledge_git_api::GitRepository::refresh(backend, source_id).is_err() {
|
||||
return None;
|
||||
}
|
||||
let reference = knowledge_git_api::GitReference::new("refs/heads/main");
|
||||
let revision_result = knowledge_git_api::GitRepository::resolve_revision(backend, source_id, &reference);
|
||||
match revision_result {
|
||||
Ok(value) => return Some(value),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn mutation_path(value: &str) -> Option<knowledge_git_api::GitMutationPath> {
|
||||
let result = knowledge_git_api::GitMutationPath::new(value);
|
||||
match result {
|
||||
Ok(value) => return Some(value),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn commit_identity() -> Option<knowledge_git_api::GitCommitIdentity> {
|
||||
let result = knowledge_git_api::GitCommitIdentity::new("Knowledge Writer", "writer@example.invalid");
|
||||
match result {
|
||||
Ok(value) => return Some(value),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn branch(value: &str) -> Option<knowledge_git_api::GitBranch> {
|
||||
let result = knowledge_git_api::GitBranch::new(value);
|
||||
match result {
|
||||
Ok(value) => return Some(value),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn candidate_for(
|
||||
backend: &mut knowledge_git_lib::GitBackend,
|
||||
source_id: &knowledge_domain_lib::SourceId,
|
||||
base: knowledge_git_api::GitRevision,
|
||||
path: &str,
|
||||
content: &[u8],
|
||||
message: &str,
|
||||
) -> Option<knowledge_git_api::GitRevision> {
|
||||
let mutation = match mutation_path(path) {
|
||||
Some(value) => knowledge_git_api::GitFileMutation::Write { path: value, content: content.to_vec() },
|
||||
None => return None,
|
||||
};
|
||||
let identity = match commit_identity() {
|
||||
Some(value) => value,
|
||||
None => return None,
|
||||
};
|
||||
let request_result = knowledge_git_api::GitWriteRequest::new(base, vec![mutation], identity, message);
|
||||
let request = match request_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return None,
|
||||
};
|
||||
let candidate_result = knowledge_git_api::GitRepositoryWriter::create_candidate(backend, source_id, &request);
|
||||
match candidate_result {
|
||||
Ok(value) => return Some(value),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guarded_push_updates_expected_branch_and_creates_only_expected_absent_branch() {
|
||||
let root = test_root("success");
|
||||
let remote_result = prepare_remote(&root);
|
||||
assert!(remote_result.is_some());
|
||||
let (remote, _) = match remote_result {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let backend_result = configured_backend(&root, &remote);
|
||||
assert!(backend_result.is_some());
|
||||
let (mut backend, source_id) = match backend_result {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let base = match resolved_main(&mut backend, &source_id) {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let candidate = match candidate_for(&mut backend, &source_id, base.clone(), "content.txt", b"second\n", "second") {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let main = match branch("refs/heads/main") {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let push = knowledge_git_api::GitPushRequest::new(main, knowledge_git_api::GitRemoteState::Revision(base), candidate.clone());
|
||||
let outcome = knowledge_git_api::GitRepositoryWriter::push_candidate(&mut backend, &source_id, &push);
|
||||
assert_eq!(outcome, Ok(knowledge_git_api::GitPushOutcome::Updated(candidate.clone())));
|
||||
assert_eq!(run_git_dir_output(&remote, "refs/heads/main"), Some(candidate.as_str().to_owned()));
|
||||
let proposal_candidate = match candidate_for(&mut backend, &source_id, candidate, "proposal.txt", b"proposal\n", "proposal") {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let proposal = match branch("refs/heads/proposal") {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let create = knowledge_git_api::GitPushRequest::new(proposal, knowledge_git_api::GitRemoteState::Absent, proposal_candidate.clone());
|
||||
let create_outcome = knowledge_git_api::GitRepositoryWriter::push_candidate(&mut backend, &source_id, &create);
|
||||
assert_eq!(create_outcome, Ok(knowledge_git_api::GitPushOutcome::Updated(proposal_candidate.clone())));
|
||||
assert_eq!(run_git_dir_output(&remote, "refs/heads/proposal"), Some(proposal_candidate.as_str().to_owned()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guarded_push_returns_conflict_after_concurrent_remote_change_without_overwrite() {
|
||||
let root = test_root("conflict");
|
||||
let remote_result = prepare_remote(&root);
|
||||
assert!(remote_result.is_some());
|
||||
let (remote, seed) = match remote_result {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let backend_result = configured_backend(&root, &remote);
|
||||
assert!(backend_result.is_some());
|
||||
let (mut backend, source_id) = match backend_result {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let base = match resolved_main(&mut backend, &source_id) {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let candidate = match candidate_for(&mut backend, &source_id, base.clone(), "content.txt", b"candidate\n", "candidate") {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
assert!(std::fs::write(seed.join("content.txt"), "concurrent\n").is_ok());
|
||||
assert!(run_git(&seed, &["add", "content.txt"]));
|
||||
assert!(run_git(&seed, &["commit", "-m", "concurrent"]));
|
||||
assert!(run_git(&seed, &["push", "origin", "main"]));
|
||||
let concurrent_text = match run_git_dir_output(&remote, "refs/heads/main") {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let concurrent = knowledge_git_api::GitRevision::new(concurrent_text.as_str());
|
||||
assert_ne!(concurrent, base);
|
||||
let main = match branch("refs/heads/main") {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let request = knowledge_git_api::GitPushRequest::new(main, knowledge_git_api::GitRemoteState::Revision(base), candidate);
|
||||
let outcome = knowledge_git_api::GitRepositoryWriter::push_candidate(&mut backend, &source_id, &request);
|
||||
assert_eq!(outcome, Ok(knowledge_git_api::GitPushOutcome::Conflict { actual: knowledge_git_api::GitRemoteState::Revision(concurrent.clone()) }));
|
||||
assert_eq!(run_git_dir_output(&remote, "refs/heads/main"), Some(concurrent.as_str().to_owned()));
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
return;
|
||||
}
|
||||
Reference in New Issue
Block a user