use log::debug;
use serde::{Deserialize, Serialize};
use radicle::cob::object::Storage;
use radicle::cob::stream::{CobRange, CobStream, Stream};
use radicle::cob::TypeName;
use radicle::profile::{Aliases, Profile};
use radicle::storage::git::paths;
use radicle::storage::ReadStorage;
use radicle::{issue, patch};
use crate::entry::{ActionEntry, Author};
use crate::storage::FeedStorage;
pub struct FeedProcessor<S: FeedStorage> {
storage: S,
profile: Profile,
}
impl<S: FeedStorage> FeedProcessor<S> {
pub fn new(mut storage: S, profile: Profile) -> Result<Self, S::Error> {
storage.initialize()?;
Ok(Self { storage, profile })
}
pub fn process_repositories(&mut self) -> Result<ProcessingStats, Box<dyn std::error::Error>> {
let aliases = self.profile.aliases();
let profile_storage = self.profile.storage.clone();
let repos = profile_storage
.repositories()?
.into_iter()
.collect::<Vec<_>>();
let mut stats = ProcessingStats::default();
let mut new_operations = Vec::new();
for repo in repos {
debug!("Processing repository: {}", repo.rid);
stats.repositories_processed += 1;
let path = paths::repository(&profile_storage, &repo.rid);
let repo_handle = radicle::storage::git::Repository::open(path.clone(), repo.rid)?;
let git2_repo = radicle::git::raw::Repository::open(path)?;
let patches = radicle::patch::Patches::open(&repo_handle)?;
let patch_stats = self.process_cobs::<patch::Action, _>(
patches.as_ref(),
&patch::TYPENAME,
&git2_repo,
&repo.rid,
&aliases,
&mut new_operations,
)?;
stats.operations_found += patch_stats.operations_found;
let issues = radicle::issue::Issues::open(&repo_handle)?;
let issue_stats = self.process_cobs::<issue::Action, _>(
issues.as_ref(),
&issue::TYPENAME,
&git2_repo,
&repo.rid,
&aliases,
&mut new_operations,
)?;
stats.operations_found += issue_stats.operations_found;
}
if !new_operations.is_empty() {
debug!(
"Inserting {} new operations into storage",
new_operations.len()
);
self.storage.insert_batch(&new_operations)?;
stats.operations_stored = new_operations.len();
} else {
debug!("No new operations found");
}
Ok(stats)
}
pub fn process_cobs<A, Store>(
&mut self,
store: &Store,
typename: &TypeName,
git2_repo: &radicle::git::raw::Repository,
rid: &radicle::prelude::RepoId,
aliases: &Aliases,
new_actions: &mut Vec<ActionEntry>,
) -> Result<ProcessingStats, Box<dyn std::error::Error>>
where
A: Serialize + for<'de> Deserialize<'de>,
Store: Storage,
{
let mut stats = ProcessingStats::default();
let cob_ids = store.types(typename)?;
for cob_id in cob_ids.into_keys() {
debug!("Processing {}: {}", typename, cob_id);
let last_operation_id = self
.storage
.get_last_processed_operation(rid, &cob_id, typename)?;
if let Some(ref since_id) = last_operation_id {
debug!("Last processed operation ID: {}", since_id);
} else {
debug!("No previous operations found (first time processing)");
}
let stream = Stream::<A>::new(
git2_repo,
CobRange::new(typename, &cob_id),
typename.clone(),
);
let stream_entries = if let Some(since_id) = last_operation_id {
let since_entries: Vec<_> = stream
.since(since_id)?
.filter_map(|s| s.ok())
.filter(|entry| entry.id() != since_id) .collect();
debug!(
"Found {} new operations since last processed",
since_entries.len(),
);
since_entries
} else {
let all_entries: Vec<_> = stream.all()?.filter_map(|s| s.ok()).collect();
debug!("Found {} total operations", all_entries.len());
all_entries
};
if stream_entries.is_empty() {
debug!("No new operations to process");
continue;
}
let mut last_processed_id = last_operation_id;
let mut operations_added = 0;
for stream_entry in stream_entries {
debug!("Processing operation: {}", stream_entry.id());
if self.storage.operation_exists(&stream_entry.id())? {
debug!("Operation already exists in storage, skipping");
continue;
}
for action in &stream_entry.actions {
new_actions.push(ActionEntry {
operation_id: stream_entry.id(),
cob_id,
rid: *rid,
timestamp: stream_entry.timestamp,
action: serde_json::to_string(action)?,
author: Author::new(&stream_entry.author.into(), aliases),
typename: typename.clone(),
});
operations_added += 1;
stats.operations_found += 1;
}
last_processed_id = Some(stream_entry.id());
}
debug!("Added {} new operations", operations_added);
if let Some(latest_id) = last_processed_id {
self.storage
.update_last_processed_operation(rid, &cob_id, typename, &latest_id)?;
debug!("Updated last processed ID to: {}", latest_id);
}
}
Ok(stats)
}
pub fn storage(&self) -> &S {
&self.storage
}
}
#[derive(Debug, Default)]
pub struct ProcessingStats {
pub repositories_processed: usize,
pub operations_found: usize,
pub operations_stored: usize,
}
impl std::fmt::Display for ProcessingStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "=== Processing Statistics ===")?;
writeln!(f, "Repositories processed: {}", self.repositories_processed)?;
writeln!(f, "Operations found: {}", self.operations_found)?;
writeln!(f, "Operations stored: {}", self.operations_stored)?;
Ok(())
}
}
pub fn process_with_memory(
) -> Result<(ProcessingStats, Vec<ActionEntry>), Box<dyn std::error::Error>> {
use crate::storage::MemoryStorage;
let profile = Profile::load()?;
let storage = MemoryStorage::new();
let mut processor = FeedProcessor::new(storage, profile)?;
let stats = processor.process_repositories()?;
let actions = processor.storage().load_all_sorted()?;
Ok((stats, actions))
}