1mod cobs;
2mod ops;
3mod repos;
4
5use std::str::FromStr;
6
7use radicle::cob::{ObjectId, Op, TypeName};
8use radicle::profile::{Aliases, Profile};
9use radicle::{issue, patch};
10use snafu::ResultExt;
11
12use crate::models::entry::TimelineEntry;
13use crate::storage::FeedStorage;
14
15pub struct FeedProcessor<'a, S: FeedStorage> {
16 storage: &'a mut S,
17 profile: Profile,
18}
19
20impl<'a, S: FeedStorage> FeedProcessor<'a, S> {
21 pub fn new(storage: &'a mut S, profile: Profile) -> Result<Self, S::Error> {
22 Ok(Self { storage, profile })
23 }
24
25 pub fn process_repository(
26 &mut self,
27 repo: &radicle::storage::RepositoryInfo,
28 ) -> Result<(), snafu::Whatever> {
29 let (git2_repo, patches, issues, aliases) = {
30 let repo_loader = repos::RepositoryLoader::new(&self.profile)?;
31 let (repo_handle, git2_repo) = repo_loader.open_repository(&repo.rid)?;
32 let patches = repo_loader.load_patches(&repo_handle)?;
33 let issues = repo_loader.load_issues(&repo_handle)?;
34 let aliases = self.profile.aliases();
35 (git2_repo, patches, issues, aliases)
36 };
37
38 for patch_id in patches {
40 self.process_single_cob::<patch::Action>(
41 &patch_id,
42 &patch::TYPENAME,
43 &git2_repo,
44 &repo.rid,
45 &aliases,
46 )?;
47 }
48
49 for issue_id in issues {
51 self.process_single_cob::<issue::Action>(
52 &issue_id,
53 &issue::TYPENAME,
54 &git2_repo,
55 &repo.rid,
56 &aliases,
57 )?;
58 }
59
60 Ok(())
61 }
62
63 fn process_single_cob<A>(
64 &mut self,
65 id: &ObjectId,
66 typename: &TypeName,
67 git2_repo: &radicle::git::raw::Repository,
68 rid: &radicle::prelude::RepoId,
69 aliases: &Aliases,
70 ) -> Result<(), snafu::Whatever>
71 where
72 A: serde::Serialize + for<'de> serde::Deserialize<'de> + Clone,
73 {
74 let last_operation_id = self
76 .storage
77 .get_last_processed_operation(rid, &id, typename)
78 .whatever_context("Unable to get last processed operation")?
79 .and_then(|oid| {
80 let oid: radicle::git::Oid = radicle::git::Oid::from_str(&oid).unwrap();
83 Op::<A>::load(git2_repo, oid).map(|op| op.id()).ok()
84 });
85
86 let stream_processor = cobs::CobStreamProcessor::new(git2_repo, typename, id);
88 let stream_entries = stream_processor.fetch_entries_since::<A>(last_operation_id)?;
89
90 if stream_entries.is_empty() {
91 tracing::debug!("{id} No new operations to process");
92 return Ok(());
93 }
94
95 let repo_alias = self
97 .storage
98 .resolve_rid(rid)
99 .whatever_context("Unable to resolve repo alias")?;
100
101 let operation_builder = ops::OperationBuilder::new(&aliases, rid, typename, repo_alias);
103
104 let (operations, last_processed_id) = operation_builder.build_operations::<A, _>(
105 stream_entries,
106 last_operation_id,
107 self.storage,
108 )?;
109
110 self.storage
112 .insert_timeline_entry(&TimelineEntry {
113 repo: rid.to_string(),
114 node: self.profile.id().to_string(),
115 cob_id: id.to_string(),
116 typename: typename.to_string(),
117 last_operation_id: last_processed_id.map(|l| l.to_string()),
118 operations: operations
119 .iter()
120 .map(|op| op.operation_id.clone())
121 .collect(),
122 })
123 .whatever_context("Insert timeline entry failed")?;
124
125 if !operations.is_empty() {
126 tracing::debug!("Inserting {} new operations into storage", operations.len());
127 self.storage
128 .insert_batch(&operations)
129 .whatever_context("Failed insert batch")?;
130 }
131
132 Ok(())
133 }
134
135 pub fn get_stats(&mut self) -> Result<crate::storage::StorageStats, S::Error> {
137 self.storage.get_stats()
138 }
139}