1use std::path::Path;
7use std::cell::RefCell;
8use std::rc::Rc;
9use git2::{Oid, Repository, FetchOptions, PushOptions, RemoteCallbacks};
10use libgrite_core::types::event::Event;
11use libgrite_core::types::ids::ActorId;
12
13use crate::wal::WalManager;
14use crate::GitError;
15
16pub const GRIT_REFSPEC: &str = "refs/grite/*:refs/grite/*";
18
19#[derive(Debug)]
21pub struct PullResult {
22 pub success: bool,
24 pub new_wal_head: Option<Oid>,
26 pub events_pulled: usize,
28 pub message: String,
30}
31
32#[derive(Debug)]
34pub struct PushResult {
35 pub success: bool,
37 pub rebased: bool,
39 pub events_rebased: usize,
41 pub message: String,
43}
44
45pub struct SyncManager {
47 repo: Repository,
48 git_dir: std::path::PathBuf,
49}
50
51impl SyncManager {
52 pub fn open(git_dir: &Path) -> Result<Self, GitError> {
54 let repo_path = git_dir.parent().ok_or(GitError::NotARepo)?;
55 let repo = Repository::open(repo_path)?;
56 Ok(Self {
57 repo,
58 git_dir: git_dir.to_path_buf(),
59 })
60 }
61
62 pub fn pull(&self, remote_name: &str) -> Result<PullResult, GitError> {
64 let wal = WalManager::open(&self.git_dir)?;
65 let old_head = wal.head()?;
66
67 let mut remote = self.repo.find_remote(remote_name)?;
69 let refspecs = [GRIT_REFSPEC];
70
71 let mut callbacks = RemoteCallbacks::new();
72 callbacks.transfer_progress(|_stats| true);
73
74 let mut fetch_options = FetchOptions::new();
75 fetch_options.remote_callbacks(callbacks);
76
77 remote.fetch(&refspecs, Some(&mut fetch_options), None)?;
78
79 let new_head = wal.head()?;
81 let events_pulled = if new_head != old_head {
82 if let Some(_new_oid) = new_head {
83 if let Some(old_oid) = old_head {
84 wal.read_since(old_oid)?.len()
85 } else {
86 wal.read_all()?.len()
87 }
88 } else {
89 0
90 }
91 } else {
92 0
93 };
94
95 Ok(PullResult {
96 success: true,
97 new_wal_head: new_head,
98 events_pulled,
99 message: if events_pulled > 0 {
100 format!("Pulled {} new events", events_pulled)
101 } else {
102 "Already up to date".to_string()
103 },
104 })
105 }
106
107 pub fn push(&self, remote_name: &str) -> Result<PushResult, GitError> {
109 let mut remote = self.repo.find_remote(remote_name)?;
110 let refspecs = [GRIT_REFSPEC];
111
112 let push_error: Rc<RefCell<Option<String>>> = Rc::new(RefCell::new(None));
113 let push_error_clone = Rc::clone(&push_error);
114
115 let mut callbacks = RemoteCallbacks::new();
116 callbacks.push_update_reference(move |refname, status| {
117 if let Some(msg) = status {
118 *push_error_clone.borrow_mut() = Some(format!("{}: {}", refname, msg));
119 }
120 Ok(())
121 });
122
123 let mut push_options = PushOptions::new();
124 push_options.remote_callbacks(callbacks);
125
126 remote.push(&refspecs, Some(&mut push_options))?;
127
128 let error = push_error.borrow().clone();
130 if let Some(error_msg) = error {
131 return Ok(PushResult {
133 success: false,
134 rebased: false,
135 events_rebased: 0,
136 message: format!("Push rejected: {}", error_msg),
137 });
138 }
139
140 Ok(PushResult {
141 success: true,
142 rebased: false,
143 events_rebased: 0,
144 message: "Push successful".to_string(),
145 })
146 }
147
148 pub fn push_with_rebase(
157 &self,
158 remote_name: &str,
159 actor_id: &ActorId,
160 ) -> Result<PushResult, GitError> {
161 let wal = WalManager::open(&self.git_dir)?;
162
163 let local_head = wal.head()?;
165
166 let result = self.push(remote_name)?;
168 if result.success {
169 return Ok(result);
170 }
171
172 let local_events = if let Some(head_oid) = local_head {
175 wal.read_from_oid(head_oid)?
176 } else {
177 vec![]
178 };
179
180 self.pull(remote_name)?;
182
183 let remote_head = wal.head()?;
185 let remote_events = if let Some(head_oid) = remote_head {
186 wal.read_from_oid(head_oid)?
187 } else {
188 vec![]
189 };
190
191 let remote_event_ids: std::collections::HashSet<_> =
193 remote_events.iter().map(|e| e.event_id).collect();
194 let unique_local_events: Vec<Event> = local_events
195 .into_iter()
196 .filter(|e| !remote_event_ids.contains(&e.event_id))
197 .collect();
198
199 let events_rebased = unique_local_events.len();
201 if !unique_local_events.is_empty() {
202 wal.append(actor_id, &unique_local_events)?;
203 }
204
205 let retry_result = self.push(remote_name)?;
207
208 Ok(PushResult {
209 success: retry_result.success,
210 rebased: true,
211 events_rebased,
212 message: if retry_result.success {
213 format!("Push successful after rebase ({} events rebased)", events_rebased)
214 } else {
215 retry_result.message
216 },
217 })
218 }
219
220 pub fn sync(&self, remote_name: &str) -> Result<(PullResult, PushResult), GitError> {
222 let pull_result = self.pull(remote_name)?;
223 let push_result = self.push(remote_name)?;
224 Ok((pull_result, push_result))
225 }
226
227 pub fn sync_with_rebase(
229 &self,
230 remote_name: &str,
231 actor_id: &ActorId,
232 ) -> Result<(PullResult, PushResult), GitError> {
233 let pull_result = self.pull(remote_name)?;
234 let push_result = self.push_with_rebase(remote_name, actor_id)?;
235 Ok((pull_result, push_result))
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 #[test]
245 fn test_sync_manager_opens() {
246 use tempfile::TempDir;
247 use std::process::Command;
248
249 let temp = TempDir::new().unwrap();
250 Command::new("git")
251 .args(["init"])
252 .current_dir(temp.path())
253 .output()
254 .unwrap();
255
256 let git_dir = temp.path().join(".git");
257 let mgr = super::SyncManager::open(&git_dir);
258 assert!(mgr.is_ok());
259 }
260}