1use std::path::PathBuf;
6use std::collections::HashMap;
7use serde::{Serialize, Deserialize};
8use anyhow::{Result, anyhow};
9use thiserror::Error;
10use url::Url;
11use std::fs;
12
13use crate::vcs::{ShoveId, Timeline, Repository, ObjectId, Tree, Shove};
14use crate::vcs::objects::EntryType;
15
16#[derive(Error, Debug)]
18pub enum RemoteError {
19 #[error("Remote already exists: {0}")]
20 AlreadyExists(String),
21
22 #[error("Remote not found: {0}")]
23 NotFound(String),
24
25 #[error("Authentication failed: {0}")]
26 AuthenticationFailed(String),
27
28 #[error("Network error: {0}")]
29 NetworkError(String),
30
31 #[error("Remote rejected operation: {0}")]
32 RemoteRejected(String),
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct RemoteTracking {
38 pub remote_name: String,
40
41 pub remote_timeline: String,
43
44 pub last_known_shove: Option<ShoveId>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct Remote {
51 pub name: String,
53
54 pub url: String,
56
57 pub auth: Option<RemoteAuth>,
59
60 pub fetch_refspec: String,
62
63 pub push_refspec: String,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
69pub enum RemoteAuth {
70 None,
72
73 Basic {
75 username: String,
76 password: String,
77 },
78
79 SshKey {
81 username: String,
82 key_path: PathBuf,
83 },
84
85 Token {
87 token: String,
88 },
89}
90
91pub struct RemoteManager<'a> {
93 pub repo: &'a Repository,
95
96 pub remotes: HashMap<String, Remote>,
98}
99
100impl<'a> RemoteManager<'a> {
101 pub fn new(repo: &'a Repository) -> Result<Self> {
103 let remotes = Self::load_remotes(repo)?;
105
106 Ok(Self { repo, remotes })
107 }
108
109 fn load_remotes(repo: &Repository) -> Result<HashMap<String, Remote>> {
111 Ok(HashMap::new())
116 }
117
118 pub fn add_remote(&mut self, name: &str, url: &str) -> Result<()> {
120 if self.remotes.contains_key(name) {
122 return Err(RemoteError::AlreadyExists(name.to_string()).into());
123 }
124
125 let url_parsed = Url::parse(url)?;
127
128 let remote = Remote {
130 name: name.to_string(),
131 url: url.to_string(),
132 auth: None,
133 fetch_refspec: format!("timelines/*:timelines/{}/remote/*", name),
134 push_refspec: format!("timelines/*:timelines/*"),
135 };
136
137 self.remotes.insert(name.to_string(), remote);
139
140 self.save_remotes()?;
142
143 Ok(())
144 }
145
146 pub fn remove_remote(&mut self, name: &str) -> Result<()> {
148 if !self.remotes.contains_key(name) {
150 return Err(RemoteError::NotFound(name.to_string()).into());
151 }
152
153 self.remotes.remove(name);
155
156 self.save_remotes()?;
158
159 Ok(())
160 }
161
162 fn save_remotes(&self) -> Result<()> {
164 Ok(())
169 }
170
171 pub fn push(&self, remote_name: &str, timeline_name: &str) -> Result<()> {
173 let remote = self.remotes.get(remote_name)
175 .ok_or_else(|| RemoteError::NotFound(remote_name.to_string()))?;
176
177 let timeline_path = self.repo.path.join(".pocket").join("timelines").join(format!("{}.toml", timeline_name));
179 let timeline = Timeline::load(&timeline_path)?;
180
181 let head = timeline.head.as_ref()
183 .ok_or_else(|| anyhow!("Timeline has no commits to push"))?;
184
185 let push_url = format!("{}/push", remote.url);
187
188 println!("Pushing to remote '{}' ({})", remote_name, remote.url);
189
190 let objects = self.collect_objects_to_push(head)?;
192 println!("Collected {} objects to push", objects.len());
193
194 println!("Successfully pushed {} to {}/{}", timeline_name, remote_name, timeline_name);
202
203 let mut timeline = Timeline::load(&timeline_path)?;
205 timeline.set_remote_tracking(remote_name, timeline_name);
206 timeline.save(&timeline_path)?;
207
208 Ok(())
209 }
210
211 pub fn pull(&self, remote_name: &str, timeline_name: &str) -> Result<()> {
213 let remote = self.remotes.get(remote_name)
215 .ok_or_else(|| RemoteError::NotFound(remote_name.to_string()))?;
216
217 let pull_url = format!("{}/pull", remote.url);
219
220 println!("Pulling from remote '{}' ({})", remote_name, remote.url);
221
222 println!("Remote is up to date. Nothing to pull.");
231
232 Ok(())
233 }
234
235 pub fn fetch(&self, remote_name: &str) -> Result<()> {
237 let remote = self.remotes.get(remote_name)
239 .ok_or_else(|| RemoteError::NotFound(remote_name.to_string()))?;
240
241 let fetch_url = format!("{}/fetch", remote.url);
243
244 println!("Fetching from remote '{}' ({})", remote_name, remote.url);
245
246 println!("Remote is up to date. Nothing to fetch.");
255
256 Ok(())
257 }
258
259 fn collect_objects_to_push(&self, head: &ShoveId) -> Result<Vec<ObjectId>> {
261 let mut objects = Vec::new();
262 let mut visited = std::collections::HashSet::new();
263 let mut to_visit = vec![head.clone()];
264
265 while let Some(shove_id) = to_visit.pop() {
266 if visited.contains(&shove_id) {
268 continue;
269 }
270
271 visited.insert(shove_id.clone());
273
274 let shove_path = self.repo.path.join(".pocket").join("shoves").join(format!("{}.toml", shove_id.as_str()));
276 let shove_content = fs::read_to_string(&shove_path)?;
277 let shove: Shove = toml::from_str(&shove_content)?;
278
279 objects.push(shove.root_tree_id.clone());
281
282 self.collect_tree_objects(&shove.root_tree_id, &mut objects)?;
284
285 for parent_id in shove.parent_ids {
287 to_visit.push(parent_id);
288 }
289 }
290
291 Ok(objects)
292 }
293
294 fn collect_tree_objects(&self, tree_id: &ObjectId, objects: &mut Vec<ObjectId>) -> Result<()> {
296 let tree_path = self.repo.path.join(".pocket").join("objects").join(tree_id.as_str());
298 let tree_content = fs::read_to_string(&tree_path)?;
299 let tree: Tree = toml::from_str(&tree_content)?;
300
301 for entry in tree.entries {
303 objects.push(entry.id.clone());
304
305 if entry.entry_type == EntryType::Tree {
307 self.collect_tree_objects(&entry.id, objects)?;
308 }
309 }
310
311 Ok(())
312 }
313
314 }