mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 15:08:24 +00:00
699 lines
24 KiB
Rust
699 lines
24 KiB
Rust
//! Helper functions for purgatory state event processing.
|
|
//!
|
|
//! These functions handle the late-binding extraction and matching of git refs
|
|
//! from state events. Refs are extracted at git push time rather than event
|
|
//! arrival time to enable flexible matching logic.
|
|
//!
|
|
//! ## Key Functions
|
|
//!
|
|
//! - [`can_satisfy_state`]: Used for **push authorization** - checks if a push
|
|
//! would transform the current refs into the declared state. This validates
|
|
//! that the pushed refspecs match what the state event declares.
|
|
//!
|
|
//! - [`can_apply_state`]: Used for **purgatory processing** - checks if we have
|
|
//! all the git OIDs needed to apply a state event. This validates that the
|
|
//! git data is available locally, regardless of current ref state.
|
|
|
|
use super::{RefPair, RefUpdate};
|
|
use nostr_sdk::prelude::*;
|
|
use std::collections::HashMap;
|
|
use std::path::Path;
|
|
|
|
use crate::git::oid_exists;
|
|
|
|
/// Extract ref pairs from a state event (kind 30618).
|
|
///
|
|
/// Parses all `refs/heads/*` and `refs/tags/*` tags from the event,
|
|
/// creating RefPair instances with the full ref name and target object SHA.
|
|
///
|
|
/// # Arguments
|
|
/// * `event` - The state event to extract refs from
|
|
///
|
|
/// # Returns
|
|
/// Vector of RefPair instances, one for each ref tag found
|
|
///
|
|
/// # Tag Format
|
|
/// State events use custom tags where the tag kind is the ref name:
|
|
/// - Tag kind: "refs/heads/main" or "refs/tags/v1.0"
|
|
/// - First value: commit SHA or annotated tag SHA
|
|
///
|
|
/// # Example
|
|
/// ```ignore
|
|
/// // Event with tags:
|
|
/// // ["refs/heads/main", "abc123..."]
|
|
/// // ["refs/tags/v1.0", "def456..."]
|
|
/// let refs = extract_refs_from_state(&event);
|
|
/// // Returns: [
|
|
/// // RefPair { ref_name: "refs/heads/main", object_sha: "abc123..." },
|
|
/// // RefPair { ref_name: "refs/tags/v1.0", object_sha: "def456..." }
|
|
/// // ]
|
|
/// ```
|
|
pub fn extract_refs_from_state(event: &Event) -> Vec<RefPair> {
|
|
event
|
|
.tags
|
|
.iter()
|
|
.filter_map(|tag| {
|
|
// Check if this is a custom tag with a ref name
|
|
if let TagKind::Custom(ref_name) = tag.kind() {
|
|
let ref_str = ref_name.as_ref();
|
|
|
|
// Only process refs/heads/* and refs/tags/*
|
|
if ref_str.starts_with("refs/heads/") || ref_str.starts_with("refs/tags/") {
|
|
// Get the object SHA (first value in tag)
|
|
let parts = tag.clone().to_vec();
|
|
if parts.len() >= 2 {
|
|
return Some(RefPair {
|
|
ref_name: ref_str.to_string(),
|
|
object_sha: parts[1].clone(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
None
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Check if a state event can be applied given the available git data.
|
|
///
|
|
/// This is used for **purgatory processing** to determine if we have all the
|
|
/// git objects needed to apply a state event. Unlike `can_satisfy_state` which
|
|
/// validates push authorization, this function only checks OID availability.
|
|
///
|
|
/// Returns true if all OIDs referenced in the state event exist in the repository.
|
|
/// Symbolic refs (starting with "ref: ") are skipped as they don't require OID lookup.
|
|
///
|
|
/// # Arguments
|
|
/// * `event` - The state event to check
|
|
/// * `repo_path` - Path to the git repository to check OIDs against
|
|
///
|
|
/// # Returns
|
|
/// true if all required OIDs exist in the repository, false otherwise
|
|
///
|
|
/// # Example
|
|
/// ```ignore
|
|
/// // State event declares:
|
|
/// // refs/heads/main -> abc123
|
|
/// // refs/heads/dev -> def456
|
|
/// // refs/heads/symlink -> ref: refs/heads/main (symbolic)
|
|
/// //
|
|
/// // If abc123 and def456 exist in repo: returns true
|
|
/// // If abc123 exists but def456 doesn't: returns false
|
|
/// // The symbolic ref doesn't require an OID check
|
|
/// ```
|
|
pub fn can_apply_state(event: &Event, repo_path: &Path) -> bool {
|
|
let state_refs = extract_refs_from_state(event);
|
|
|
|
for ref_pair in state_refs {
|
|
// Skip symbolic refs (they don't require OID lookup)
|
|
if ref_pair.object_sha.starts_with("ref: ") {
|
|
continue;
|
|
}
|
|
|
|
// Check if the OID exists in the repository
|
|
if !oid_exists(repo_path, &ref_pair.object_sha) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
true
|
|
}
|
|
|
|
/// Check if a state event can be satisfied by ref updates plus local refs.
|
|
///
|
|
/// This is used for **push authorization** to validate that a push would
|
|
/// transform the current refs into the declared state.
|
|
///
|
|
/// Returns true if applying the ref updates to local state results in exactly
|
|
/// the state declared in the event. This means:
|
|
/// 1. Filter local_refs to only branches (refs/heads/*) and tags (refs/tags/*)
|
|
/// 2. Apply pushed_updates to create a "would-be" state
|
|
/// 3. Compare would-be state with event's declared state - must match exactly
|
|
///
|
|
/// This implements correct authorization: the push must transform local state
|
|
/// into the declared state, accounting for additions, deletions, and modifications.
|
|
///
|
|
/// # Arguments
|
|
/// * `event` - The state event to check
|
|
/// * `pushed_updates` - Ref updates in the current push operation
|
|
/// * `local_refs` - Refs already existing locally (ref_name -> SHA)
|
|
///
|
|
/// # Returns
|
|
/// true if push transforms local state into declared state, false otherwise
|
|
///
|
|
/// # Example
|
|
/// ```ignore
|
|
/// // State event declares: refs/heads/main@abc123
|
|
/// // Local: refs/heads/main@old123, refs/heads/dev@def456
|
|
/// // Push updates: main old123->abc123, dev def456->0000 (delete)
|
|
/// // Result: false (event doesn't declare dev deletion)
|
|
/// ```
|
|
pub fn can_satisfy_state(
|
|
event: &Event,
|
|
pushed_updates: &[RefUpdate],
|
|
local_refs: &HashMap<String, String>,
|
|
) -> bool {
|
|
let state_refs = extract_refs_from_state(event);
|
|
|
|
// Filter local_refs to only branches and tags
|
|
let mut would_be_state: HashMap<String, String> = local_refs
|
|
.iter()
|
|
.filter(|(ref_name, _)| {
|
|
ref_name.starts_with("refs/heads/") || ref_name.starts_with("refs/tags/")
|
|
})
|
|
.map(|(k, v)| (k.clone(), v.clone()))
|
|
.collect();
|
|
|
|
// Apply all pushed updates to create the would-be state
|
|
for update in pushed_updates {
|
|
// Only process branches and tags
|
|
if !update.ref_name.starts_with("refs/heads/") && !update.ref_name.starts_with("refs/tags/")
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if update.is_deletion() {
|
|
// Remove from would-be state
|
|
would_be_state.remove(&update.ref_name);
|
|
} else {
|
|
// Create or modify in would-be state
|
|
would_be_state.insert(update.ref_name.clone(), update.new_oid.clone());
|
|
}
|
|
}
|
|
|
|
// Convert event's state refs to a HashMap for comparison
|
|
let declared_state: HashMap<String, String> = state_refs
|
|
.into_iter()
|
|
.map(|r| (r.ref_name, r.object_sha))
|
|
.collect();
|
|
|
|
// would_be_state must exactly match declared_state
|
|
would_be_state == declared_state
|
|
}
|
|
|
|
/// Get refs from state event that aren't in pushed_refs.
|
|
///
|
|
/// Returns refs that need to be present but aren't being pushed.
|
|
/// These refs should exist in local_refs for the state to be satisfiable.
|
|
/// Useful for error messages showing what's missing.
|
|
///
|
|
/// # Arguments
|
|
/// * `event` - The state event to check
|
|
/// * `pushed_refs` - Refs being pushed in the current operation
|
|
///
|
|
/// # Returns
|
|
/// Vector of RefPair instances for refs not in pushed_refs
|
|
///
|
|
/// # Example
|
|
/// ```ignore
|
|
/// // State event declares: refs/heads/main@abc123, refs/heads/dev@def456
|
|
/// // Pushed: refs/heads/main@abc123
|
|
/// // Result: [RefPair { ref_name: "refs/heads/dev", object_sha: "def456" }]
|
|
/// ```
|
|
pub fn get_unpushed_refs(event: &Event, pushed_refs: &[RefPair]) -> Vec<RefPair> {
|
|
let state_refs = extract_refs_from_state(event);
|
|
|
|
state_refs
|
|
.into_iter()
|
|
.filter(|state_ref| {
|
|
// Include if NOT in pushed_refs (by name and SHA)
|
|
!pushed_refs.iter().any(|pushed_ref| {
|
|
pushed_ref.ref_name == state_ref.ref_name
|
|
&& pushed_ref.object_sha == state_ref.object_sha
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use nostr_sdk::{EventBuilder, Keys, Tag};
|
|
|
|
fn create_test_state_event(identifier: &str, refs: Vec<(&str, &str)>) -> Event {
|
|
let keys = Keys::generate();
|
|
let mut tags = vec![Tag::custom(TagKind::d(), vec![identifier.to_string()])];
|
|
|
|
for (ref_name, sha) in refs {
|
|
tags.push(Tag::custom(
|
|
TagKind::custom(ref_name),
|
|
vec![sha.to_string()],
|
|
));
|
|
}
|
|
|
|
EventBuilder::new(Kind::from(30618), "")
|
|
.tags(tags)
|
|
.sign_with_keys(&keys)
|
|
.unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_refs_from_state() {
|
|
let event = create_test_state_event(
|
|
"test-repo",
|
|
vec![
|
|
("refs/heads/main", "abc123"),
|
|
("refs/heads/dev", "def456"),
|
|
("refs/tags/v1.0", "789xyz"),
|
|
],
|
|
);
|
|
|
|
let refs = extract_refs_from_state(&event);
|
|
|
|
assert_eq!(refs.len(), 3);
|
|
assert!(refs
|
|
.iter()
|
|
.any(|r| r.ref_name == "refs/heads/main" && r.object_sha == "abc123"));
|
|
assert!(refs
|
|
.iter()
|
|
.any(|r| r.ref_name == "refs/heads/dev" && r.object_sha == "def456"));
|
|
assert!(refs
|
|
.iter()
|
|
.any(|r| r.ref_name == "refs/tags/v1.0" && r.object_sha == "789xyz"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_extract_refs_ignores_non_ref_tags() {
|
|
let keys = Keys::generate();
|
|
let tags = vec![
|
|
Tag::custom(TagKind::d(), vec!["test-repo".to_string()]),
|
|
Tag::custom(
|
|
TagKind::custom("refs/heads/main"),
|
|
vec!["abc123".to_string()],
|
|
),
|
|
Tag::custom(TagKind::custom("some-other-tag"), vec!["value".to_string()]),
|
|
];
|
|
|
|
let event = EventBuilder::new(Kind::from(30618), "")
|
|
.tags(tags)
|
|
.sign_with_keys(&keys)
|
|
.unwrap();
|
|
|
|
let refs = extract_refs_from_state(&event);
|
|
|
|
// Should only extract the refs/heads/main tag
|
|
assert_eq!(refs.len(), 1);
|
|
assert_eq!(refs[0].ref_name, "refs/heads/main");
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_satisfy_state_all_in_pushed() {
|
|
let event = create_test_state_event(
|
|
"test-repo",
|
|
vec![("refs/heads/main", "abc123"), ("refs/heads/dev", "def456")],
|
|
);
|
|
|
|
let pushed_updates = vec![
|
|
RefUpdate {
|
|
old_oid: "0000000000000000000000000000000000000000".to_string(),
|
|
new_oid: "abc123".to_string(),
|
|
ref_name: "refs/heads/main".to_string(),
|
|
},
|
|
RefUpdate {
|
|
old_oid: "0000000000000000000000000000000000000000".to_string(),
|
|
new_oid: "def456".to_string(),
|
|
ref_name: "refs/heads/dev".to_string(),
|
|
},
|
|
];
|
|
|
|
let local_refs = HashMap::new();
|
|
|
|
assert!(can_satisfy_state(&event, &pushed_updates, &local_refs));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_satisfy_state_split_between_pushed_and_local() {
|
|
let event = create_test_state_event(
|
|
"test-repo",
|
|
vec![("refs/heads/main", "abc123"), ("refs/heads/dev", "def456")],
|
|
);
|
|
|
|
let pushed_updates = vec![RefUpdate {
|
|
old_oid: "0000000000000000000000000000000000000000".to_string(),
|
|
new_oid: "abc123".to_string(),
|
|
ref_name: "refs/heads/main".to_string(),
|
|
}];
|
|
|
|
let mut local_refs = HashMap::new();
|
|
local_refs.insert("refs/heads/dev".to_string(), "def456".to_string());
|
|
|
|
assert!(can_satisfy_state(&event, &pushed_updates, &local_refs));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_satisfy_state_missing_ref() {
|
|
let event = create_test_state_event(
|
|
"test-repo",
|
|
vec![("refs/heads/main", "abc123"), ("refs/heads/dev", "def456")],
|
|
);
|
|
|
|
let pushed_updates = vec![RefUpdate {
|
|
old_oid: "0000000000000000000000000000000000000000".to_string(),
|
|
new_oid: "abc123".to_string(),
|
|
ref_name: "refs/heads/main".to_string(),
|
|
}];
|
|
|
|
let local_refs = HashMap::new();
|
|
|
|
// dev ref is missing
|
|
assert!(!can_satisfy_state(&event, &pushed_updates, &local_refs));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_satisfy_state_modification() {
|
|
let event = create_test_state_event(
|
|
"test-repo",
|
|
vec![("refs/heads/main", "abc123"), ("refs/heads/dev", "def456")],
|
|
);
|
|
|
|
let pushed_updates = vec![
|
|
RefUpdate {
|
|
old_oid: "old123".to_string(),
|
|
new_oid: "abc123".to_string(),
|
|
ref_name: "refs/heads/main".to_string(),
|
|
},
|
|
RefUpdate {
|
|
old_oid: "wrong-sha".to_string(),
|
|
new_oid: "def456".to_string(),
|
|
ref_name: "refs/heads/dev".to_string(),
|
|
},
|
|
];
|
|
|
|
let mut local_refs = HashMap::new();
|
|
local_refs.insert("refs/heads/main".to_string(), "old123".to_string());
|
|
local_refs.insert("refs/heads/dev".to_string(), "wrong-sha".to_string());
|
|
|
|
// Should succeed because push updates both to match event
|
|
assert!(can_satisfy_state(&event, &pushed_updates, &local_refs));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_satisfy_state_rejects_extra_refs() {
|
|
let event = create_test_state_event("test-repo", vec![("refs/heads/main", "abc123")]);
|
|
|
|
let pushed_updates = vec![
|
|
RefUpdate {
|
|
old_oid: "0000000000000000000000000000000000000000".to_string(),
|
|
new_oid: "abc123".to_string(),
|
|
ref_name: "refs/heads/main".to_string(),
|
|
},
|
|
RefUpdate {
|
|
old_oid: "old456".to_string(),
|
|
new_oid: "def456".to_string(),
|
|
ref_name: "refs/heads/dev".to_string(),
|
|
},
|
|
];
|
|
|
|
let mut local_refs = HashMap::new();
|
|
local_refs.insert("refs/heads/dev".to_string(), "old456".to_string());
|
|
|
|
// Should fail because event doesn't declare dev
|
|
assert!(!can_satisfy_state(&event, &pushed_updates, &local_refs));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_satisfy_state_filters_non_branch_tag_refs() {
|
|
let event = create_test_state_event("test-repo", vec![("refs/heads/main", "abc123")]);
|
|
|
|
let pushed_updates = vec![RefUpdate {
|
|
old_oid: "0000000000000000000000000000000000000000".to_string(),
|
|
new_oid: "abc123".to_string(),
|
|
ref_name: "refs/heads/main".to_string(),
|
|
}];
|
|
|
|
let mut local_refs = HashMap::new();
|
|
// Add some non-branch/non-tag refs that should be filtered out
|
|
local_refs.insert("refs/pull/123/head".to_string(), "xyz789".to_string());
|
|
local_refs.insert("refs/some/other/thing".to_string(), "aaa111".to_string());
|
|
|
|
// Should succeed - non-branch/tag refs are filtered out
|
|
assert!(can_satisfy_state(&event, &pushed_updates, &local_refs));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_satisfy_state_empty_event() {
|
|
let event = create_test_state_event("test-repo", vec![]);
|
|
let pushed_refs = vec![];
|
|
let local_refs = HashMap::new();
|
|
|
|
// Empty state event is satisfied
|
|
assert!(can_satisfy_state(&event, &pushed_refs, &local_refs));
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_unpushed_refs() {
|
|
let event = create_test_state_event(
|
|
"test-repo",
|
|
vec![
|
|
("refs/heads/main", "abc123"),
|
|
("refs/heads/dev", "def456"),
|
|
("refs/tags/v1.0", "789xyz"),
|
|
],
|
|
);
|
|
|
|
let pushed_refs = vec![RefPair {
|
|
ref_name: "refs/heads/main".to_string(),
|
|
object_sha: "abc123".to_string(),
|
|
}];
|
|
|
|
let unpushed = get_unpushed_refs(&event, &pushed_refs);
|
|
|
|
assert_eq!(unpushed.len(), 2);
|
|
assert!(unpushed.iter().any(|r| r.ref_name == "refs/heads/dev"));
|
|
assert!(unpushed.iter().any(|r| r.ref_name == "refs/tags/v1.0"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_unpushed_refs_all_pushed() {
|
|
let event = create_test_state_event("test-repo", vec![("refs/heads/main", "abc123")]);
|
|
|
|
let pushed_refs = vec![RefPair {
|
|
ref_name: "refs/heads/main".to_string(),
|
|
object_sha: "abc123".to_string(),
|
|
}];
|
|
|
|
let unpushed = get_unpushed_refs(&event, &pushed_refs);
|
|
|
|
assert_eq!(unpushed.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_unpushed_refs_sha_mismatch() {
|
|
let event = create_test_state_event("test-repo", vec![("refs/heads/main", "abc123")]);
|
|
|
|
let pushed_refs = vec![RefPair {
|
|
ref_name: "refs/heads/main".to_string(),
|
|
object_sha: "different-sha".to_string(), // Different SHA
|
|
}];
|
|
|
|
let unpushed = get_unpushed_refs(&event, &pushed_refs);
|
|
|
|
// Should still be unpushed because SHA doesn't match
|
|
assert_eq!(unpushed.len(), 1);
|
|
assert_eq!(unpushed[0].ref_name, "refs/heads/main");
|
|
assert_eq!(unpushed[0].object_sha, "abc123");
|
|
}
|
|
|
|
// =========================================================================
|
|
// can_apply_state tests
|
|
// =========================================================================
|
|
|
|
/// Helper to create a temporary bare git repository with a commit.
|
|
/// Returns (temp_dir, commit_hash) where commit_hash is Some if a commit was created.
|
|
fn create_test_repo_with_commit() -> (tempfile::TempDir, Option<String>) {
|
|
use std::process::Command;
|
|
|
|
let temp_dir = tempfile::tempdir().unwrap();
|
|
let bare_path = temp_dir.path();
|
|
|
|
// Initialize bare repo
|
|
Command::new("git")
|
|
.args(["init", "--bare"])
|
|
.current_dir(bare_path)
|
|
.output()
|
|
.expect("Failed to init bare git repo");
|
|
|
|
// Create a working repo to generate a commit
|
|
let work_dir = tempfile::tempdir().unwrap();
|
|
|
|
Command::new("git")
|
|
.args(["init"])
|
|
.current_dir(work_dir.path())
|
|
.output()
|
|
.expect("Failed to init work repo");
|
|
|
|
Command::new("git")
|
|
.args(["config", "user.email", "test@test.com"])
|
|
.current_dir(work_dir.path())
|
|
.output()
|
|
.expect("Failed to set email");
|
|
|
|
Command::new("git")
|
|
.args(["config", "user.name", "Test"])
|
|
.current_dir(work_dir.path())
|
|
.output()
|
|
.expect("Failed to set name");
|
|
|
|
// Disable GPG signing for tests (prevents yubikey prompts)
|
|
Command::new("git")
|
|
.args(["config", "commit.gpgsign", "false"])
|
|
.current_dir(work_dir.path())
|
|
.output()
|
|
.expect("Failed to disable commit.gpgsign");
|
|
|
|
Command::new("git")
|
|
.args(["config", "tag.gpgsign", "false"])
|
|
.current_dir(work_dir.path())
|
|
.output()
|
|
.expect("Failed to disable tag.gpgsign");
|
|
|
|
// Create a commit
|
|
std::fs::write(work_dir.path().join("file.txt"), "content").unwrap();
|
|
Command::new("git")
|
|
.args(["add", "."])
|
|
.current_dir(work_dir.path())
|
|
.output()
|
|
.expect("Failed to add");
|
|
|
|
Command::new("git")
|
|
.args(["commit", "-m", "test"])
|
|
.current_dir(work_dir.path())
|
|
.output()
|
|
.expect("Failed to commit");
|
|
|
|
// Get the commit hash from the working repo
|
|
let output = Command::new("git")
|
|
.args(["rev-parse", "HEAD"])
|
|
.current_dir(work_dir.path())
|
|
.output()
|
|
.expect("Failed to get commit hash");
|
|
|
|
let commit_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
|
|
// Push to bare repo
|
|
Command::new("git")
|
|
.args(["push", bare_path.to_str().unwrap(), "HEAD:refs/heads/main"])
|
|
.current_dir(work_dir.path())
|
|
.output()
|
|
.expect("Failed to push");
|
|
|
|
(temp_dir, Some(commit_hash))
|
|
}
|
|
|
|
/// Helper to create an empty bare git repository (no commits).
|
|
fn create_empty_test_repo() -> tempfile::TempDir {
|
|
use std::process::Command;
|
|
|
|
let temp_dir = tempfile::tempdir().unwrap();
|
|
|
|
Command::new("git")
|
|
.args(["init", "--bare"])
|
|
.current_dir(temp_dir.path())
|
|
.output()
|
|
.expect("Failed to init bare git repo");
|
|
|
|
temp_dir
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_apply_state_with_existing_oid() {
|
|
// Create a repo with a real commit
|
|
let (temp_repo, commit_hash) = create_test_repo_with_commit();
|
|
let repo_path = temp_repo.path();
|
|
let commit_hash = commit_hash.expect("Should have a commit");
|
|
|
|
// Create a state event referencing that commit
|
|
let event = create_test_state_event("test-repo", vec![("refs/heads/main", &commit_hash)]);
|
|
|
|
// Should return true since the OID exists
|
|
assert!(can_apply_state(&event, repo_path));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_apply_state_with_missing_oid() {
|
|
// Create an empty repo
|
|
let temp_repo = create_empty_test_repo();
|
|
let repo_path = temp_repo.path();
|
|
|
|
// Create a state event referencing a non-existent commit
|
|
let event = create_test_state_event(
|
|
"test-repo",
|
|
vec![(
|
|
"refs/heads/main",
|
|
"0000000000000000000000000000000000000000",
|
|
)],
|
|
);
|
|
|
|
// Should return false since the OID doesn't exist
|
|
assert!(!can_apply_state(&event, repo_path));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_apply_state_with_symbolic_ref() {
|
|
// Create an empty repo (no commits needed for symbolic refs)
|
|
let temp_repo = create_empty_test_repo();
|
|
let repo_path = temp_repo.path();
|
|
|
|
// Create a state event with only a symbolic ref
|
|
let event = create_test_state_event(
|
|
"test-repo",
|
|
vec![("refs/heads/main", "ref: refs/heads/other")],
|
|
);
|
|
|
|
// Should return true - symbolic refs don't require OID lookup
|
|
assert!(can_apply_state(&event, repo_path));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_apply_state_mixed_existing_and_missing() {
|
|
// Create a repo with a real commit
|
|
let (temp_repo, commit_hash) = create_test_repo_with_commit();
|
|
let repo_path = temp_repo.path();
|
|
let commit_hash = commit_hash.expect("Should have a commit");
|
|
|
|
// Create a state event with one existing and one missing OID
|
|
let event = create_test_state_event(
|
|
"test-repo",
|
|
vec![
|
|
("refs/heads/main", &commit_hash), // exists
|
|
("refs/heads/dev", "0000000000000000000000000000000000000000"), // doesn't exist
|
|
],
|
|
);
|
|
|
|
// Should return false since one OID is missing
|
|
assert!(!can_apply_state(&event, repo_path));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_apply_state_empty_event() {
|
|
let temp_repo = create_empty_test_repo();
|
|
let repo_path = temp_repo.path();
|
|
|
|
// Empty state event (no refs declared)
|
|
let event = create_test_state_event("test-repo", vec![]);
|
|
|
|
// Should return true - nothing to check
|
|
assert!(can_apply_state(&event, repo_path));
|
|
}
|
|
|
|
#[test]
|
|
fn test_can_apply_state_mixed_symbolic_and_real() {
|
|
// Create a repo with a real commit
|
|
let (temp_repo, commit_hash) = create_test_repo_with_commit();
|
|
let repo_path = temp_repo.path();
|
|
let commit_hash = commit_hash.expect("Should have a commit");
|
|
|
|
// Create a state event with both a real OID and a symbolic ref
|
|
let event = create_test_state_event(
|
|
"test-repo",
|
|
vec![
|
|
("refs/heads/main", &commit_hash), // real OID that exists
|
|
("refs/heads/alias", "ref: refs/heads/main"), // symbolic ref
|
|
],
|
|
);
|
|
|
|
// Should return true - real OID exists, symbolic ref skipped
|
|
assert!(can_apply_state(&event, repo_path));
|
|
}
|
|
}
|