1use std::io::Read;
4use std::path::Path;
5use std::sync::Arc;
6
7use crate::cid::ToVoidCid;
8use crate::metadata::ShardMap;
9use crate::pipeline::{commit_workspace, CommitOptions, SealOptions};
10use crate::pipeline::{
11 push_repo, pull_repo, PushOptions, PullOptions, PushResult, PullResult, CloneMode,
12};
13use crate::refs;
14use crate::store::{FsStore, IpfsBackend, RemoteStore};
15use crate::workspace::checkout::{checkout_tree, CheckoutOptions, CheckoutStats};
16use crate::workspace::reset::{reset_paths, ResetOptions, ResetResult};
17use crate::workspace::stage::{stage_paths, StageOptions, StageResult};
18use crate::workspace::status::{status_workspace, StatusOptions, StatusResult};
19use crate::{Result, VoidContext, VoidError};
20
21use super::builder::RepoBuilder;
22use super::types::{CommitId, CommitInfo, DirEntry};
23
24#[derive(Clone)]
28pub struct Repo {
29 ctx: VoidContext,
30 remote: Option<Arc<dyn RemoteStore>>,
31}
32
33impl Repo {
34 pub fn builder(path: impl AsRef<Path>) -> RepoBuilder {
36 RepoBuilder::new(path)
37 }
38
39 pub(crate) fn from_context(ctx: VoidContext) -> Self {
41 Self { ctx, remote: None }
42 }
43
44 pub(crate) fn from_context_with_remote(
46 ctx: VoidContext,
47 remote: Option<Arc<dyn RemoteStore>>,
48 ) -> Self {
49 Self { ctx, remote }
50 }
51
52 pub fn head(&self) -> Result<CommitInfo> {
56 let commit_cid = refs::resolve_head(&self.ctx.paths.void_dir)?
57 .ok_or_else(|| VoidError::NotFound("no HEAD commit".into()))?;
58
59 let store = self.open_store()?;
60 let void_cid = commit_cid.to_void_cid()?;
61 let (commit, _reader) = self.ctx.load_commit(&store, &void_cid)?;
62
63 Ok(CommitInfo {
64 id: CommitId(commit_cid.as_bytes().to_vec()),
65 message: commit.message.clone(),
66 timestamp: commit.timestamp,
67 author: commit.author.as_ref().map(|a| a.to_hex()),
68 parents: commit
69 .parents
70 .iter()
71 .map(|p| CommitId(p.as_bytes().to_vec()))
72 .collect(),
73 })
74 }
75
76 pub fn stream_file(&self, path: &str) -> Result<impl Read + '_> {
78 self.read_file(path).map(std::io::Cursor::new)
79 }
80
81 pub fn stream_file_at(&self, commit: &CommitId, path: &str) -> Result<impl Read + '_> {
83 let commit_cid = crate::crypto::CommitCid::from_bytes(commit.0.clone());
84 let store = self.open_store()?;
85 let void_cid = commit_cid.to_void_cid()?;
86 let (commit_obj, reader) = self.ctx.load_commit(&store, &void_cid)?;
87 let ancestor_keys =
88 crate::crypto::collect_ancestor_content_keys_vault(&self.ctx.crypto.vault, &store, &commit_obj);
89 let content = self.ctx.read_file_from_commit(&store, &commit_obj, &reader, &ancestor_keys, path)?;
90 Ok(std::io::Cursor::new(content))
91 }
92
93 pub fn read_file(&self, path: &str) -> Result<Vec<u8>> {
95 let commit_cid = refs::resolve_head(&self.ctx.paths.void_dir)?
96 .ok_or_else(|| VoidError::NotFound("no HEAD commit".into()))?;
97
98 let store = self.open_store()?;
99 let void_cid = commit_cid.to_void_cid()?;
100 let (commit, reader) = self.ctx.load_commit(&store, &void_cid)?;
101 let ancestor_keys =
102 crate::crypto::collect_ancestor_content_keys_vault(&self.ctx.crypto.vault, &store, &commit);
103
104 self.ctx
105 .read_file_from_commit(&store, &commit, &reader, &ancestor_keys, path)
106 }
107
108 pub fn list_dir_at(&self, refstr: &str, path: &str) -> Result<Vec<DirEntry>> {
110 let commit_cid = self.resolve_ref(refstr)?;
111 let store = self.open_store()?;
112 let void_cid = commit_cid.to_void_cid()?;
113 let (commit, reader) = self.ctx.load_commit(&store, &void_cid)?;
114 let manifest = self.ctx.load_manifest(&store, &commit, &reader)?
115 .ok_or_else(|| VoidError::NotFound("no manifest in commit".into()))?;
116
117 let mut entries = Vec::new();
118 for entry_result in manifest.iter() {
119 let entry = entry_result?;
120 if entry.path.starts_with(path) || path == "." || path.is_empty() {
121 entries.push(DirEntry {
122 path: entry.path.clone(),
123 size: entry.size,
124 is_large: entry.shard_count > 1,
125 });
126 }
127 }
128 Ok(entries)
129 }
130
131 pub fn list_dir(&self, path: &str) -> Result<Vec<DirEntry>> {
133 let commit_cid = refs::resolve_head(&self.ctx.paths.void_dir)?
134 .ok_or_else(|| VoidError::NotFound("no HEAD commit".into()))?;
135
136 let store = self.open_store()?;
137 let void_cid = commit_cid.to_void_cid()?;
138 let (commit, reader) = self.ctx.load_commit(&store, &void_cid)?;
139
140 let manifest = self
141 .ctx
142 .load_manifest(&store, &commit, &reader)?
143 .ok_or_else(|| VoidError::NotFound("no manifest in commit".into()))?;
144
145 let mut entries = Vec::new();
146 for entry_result in manifest.iter() {
147 let entry = entry_result?;
148 if entry.path.starts_with(path) || path == "." || path.is_empty() {
149 entries.push(DirEntry {
150 path: entry.path.clone(),
151 size: entry.size,
152 is_large: entry.shard_count > 1,
153 });
154 }
155 }
156
157 Ok(entries)
158 }
159
160 pub fn status(&self) -> Result<StatusResult> {
162 self.status_with_options(vec![], None)
163 }
164
165 pub fn status_with_options(
167 &self,
168 patterns: Vec<String>,
169 observer: Option<std::sync::Arc<dyn crate::support::events::VoidObserver>>,
170 ) -> Result<StatusResult> {
171 status_workspace(StatusOptions {
172 ctx: self.ctx.clone(),
173 patterns,
174 observer,
175 })
176 }
177
178 pub fn add(&self, paths: &[&str]) -> Result<StageResult> {
182 self.add_with_options(paths, None)
183 }
184
185 pub fn add_with_options(
187 &self,
188 paths: &[&str],
189 observer: Option<std::sync::Arc<dyn crate::support::events::VoidObserver>>,
190 ) -> Result<StageResult> {
191 stage_paths(StageOptions {
192 ctx: self.ctx.clone(),
193 patterns: paths.iter().map(|p| p.to_string()).collect(),
194 observer,
195 })
196 }
197
198 pub fn commit(&self, message: &str) -> Result<CommitInfo> {
200 let parent_cid = refs::resolve_head(&self.ctx.paths.void_dir)?;
201
202 let seal_opts = SealOptions {
203 ctx: self.ctx.clone(),
204 shard_map: ShardMap::new(64),
205 content_key: None,
206 parent_content_key: None,
207 };
208
209 let commit_opts = CommitOptions {
210 seal: seal_opts,
211 message: message.to_string(),
212 parent_cid,
213 allow_data_loss: false,
214 foreign_parent: false,
215 };
216
217 let result = commit_workspace(commit_opts)?;
218
219 Ok(CommitInfo {
220 id: CommitId(result.commit_cid.as_bytes().to_vec()),
221 message: message.to_string(),
222 timestamp: 0, author: None,
224 parents: vec![],
225 })
226 }
227
228 pub fn restore(&self, paths: &[&str]) -> Result<CheckoutStats> {
230 self.restore_from_ref("HEAD", paths, false, None)
231 }
232
233 pub fn restore_from(
235 &self,
236 source: &str,
237 paths: &[&str],
238 ) -> Result<CheckoutStats> {
239 self.restore_from_ref(source, paths, false, None)
240 }
241
242 pub fn restore_from_ref(
244 &self,
245 source: &str,
246 paths: &[&str],
247 force: bool,
248 observer: Option<std::sync::Arc<dyn crate::support::events::VoidObserver>>,
249 ) -> Result<CheckoutStats> {
250 let commit_cid = self.resolve_ref(source)?;
251 let void_cid = commit_cid.to_void_cid()?;
252 let store = self.open_store()?;
253
254 let has_paths = !paths.is_empty();
255 let options = CheckoutOptions {
256 paths: if has_paths { Some(paths.iter().map(|p| p.to_string()).collect()) } else { None },
257 force: force || has_paths, observer,
259 workspace_dir: None,
260 include_large: has_paths, };
262
263 checkout_tree(&store, &self.ctx.crypto.vault, &void_cid, self.ctx.paths.root.as_std_path(), &options)
264 }
265
266 pub fn reset(&self, paths: &[&str]) -> Result<ResetResult> {
268 self.reset_with_options(paths, None)
269 }
270
271 pub fn reset_with_options(
273 &self,
274 paths: &[&str],
275 observer: Option<std::sync::Arc<dyn crate::support::events::VoidObserver>>,
276 ) -> Result<ResetResult> {
277 reset_paths(ResetOptions {
278 ctx: self.ctx.clone(),
279 patterns: paths.iter().map(|p| p.to_string()).collect(),
280 observer,
281 })
282 }
283
284 pub fn remove(
286 &self,
287 paths: &[&str],
288 cached_only: bool,
289 force: bool,
290 recursive: bool,
291 ) -> Result<crate::workspace::remove::RemoveResult> {
292 crate::workspace::remove::remove_paths(crate::workspace::remove::RemoveOptions {
293 ctx: self.ctx.clone(),
294 paths: paths.iter().map(|p| p.to_string()).collect(),
295 cached_only,
296 force,
297 recursive,
298 })
299 }
300
301 pub fn mv(&self, source: &str, dest: &str) -> Result<crate::workspace::move_path::MoveResult> {
303 crate::workspace::move_path::move_path(crate::workspace::move_path::MoveOptions {
304 ctx: self.ctx.clone(),
305 source: source.to_string(),
306 dest: dest.to_string(),
307 })
308 }
309
310 pub fn log(&self, limit: usize) -> Result<Vec<CommitInfo>> {
317 let store = self.open_store()?;
318 let walker = crate::ops::traversal::walk_from_head(
319 &store,
320 &self.ctx.crypto.vault,
321 &self.ctx.paths.void_dir,
322 Some(limit),
323 )?;
324
325 let mut commits = Vec::new();
326 for walked in walker {
327 match walked {
328 Ok(w) => {
329 commits.push(CommitInfo {
330 id: CommitId(w.cid.to_bytes()),
331 message: w.commit.message.clone(),
332 timestamp: w.commit.timestamp,
333 author: w.commit.author.as_ref().map(|a| a.to_hex()),
334 parents: w.commit.parents.iter()
335 .map(|p| CommitId(p.as_bytes().to_vec()))
336 .collect(),
337 });
338 }
339 Err(_) if !commits.is_empty() => {
340 break;
343 }
344 Err(e) => return Err(e), }
346 }
347 Ok(commits)
348 }
349
350 pub fn diff(&self) -> Result<crate::diff::TreeDiff> {
352 let commit_cid = refs::resolve_head(&self.ctx.paths.void_dir)?
353 .ok_or_else(|| VoidError::NotFound("no HEAD commit".into()))?;
354
355 let store = self.open_store()?;
356 let void_cid = commit_cid.to_void_cid()?;
357
358 crate::diff::diff_working(
359 &store,
360 &self.ctx.crypto.vault,
361 &void_cid,
362 self.ctx.paths.root.as_std_path(),
363 )
364 }
365
366 pub fn diff_staged(&self) -> Result<crate::diff::TreeDiff> {
368 let index = crate::index::read_index(
369 self.ctx.paths.void_dir.as_std_path(),
370 self.ctx.crypto.vault.index_key()?,
371 )?;
372 crate::diff::diff_staged(
373 &self.open_store()?,
374 &self.ctx.crypto.vault,
375 self.ctx.paths.void_dir.as_std_path(),
376 &index,
377 )
378 }
379
380 pub fn diff_commits(&self, from: &CommitId, to: &CommitId) -> Result<crate::diff::TreeDiff> {
382 let store = self.open_store()?;
383 let from_cid = crate::cid::from_bytes(&from.0)?;
384 let to_cid = crate::cid::from_bytes(&to.0)?;
385
386 crate::diff::diff_commits(
387 &store,
388 &self.ctx.crypto.vault,
389 Some(&from_cid),
390 &to_cid,
391 )
392 }
393
394 pub fn branches(&self) -> Result<Vec<String>> {
398 refs::list_branches(&self.ctx.paths.void_dir)
399 }
400
401 pub fn branch(&self, name: &str) -> Result<()> {
403 let commit_cid = refs::resolve_head(&self.ctx.paths.void_dir)?
404 .ok_or_else(|| VoidError::NotFound("no HEAD commit".into()))?;
405 refs::write_branch(&self.ctx.paths.void_dir, name, &commit_cid)
406 }
407
408 pub fn branch_at(&self, name: &str, commit: &CommitId) -> Result<()> {
410 let commit_cid = crate::crypto::CommitCid::from_bytes(commit.0.clone());
411 refs::write_branch(&self.ctx.paths.void_dir, name, &commit_cid)
412 }
413
414 pub fn delete_branch(&self, name: &str) -> Result<()> {
416 refs::delete_branch(&self.ctx.paths.void_dir, name)
417 }
418
419 pub fn switch(&self, name: &str) -> Result<CheckoutStats> {
421 let commit_cid = refs::read_branch(&self.ctx.paths.void_dir, name)?
422 .ok_or_else(|| VoidError::NotFound(format!("branch '{}' not found", name)))?;
423
424 let void_cid = commit_cid.to_void_cid()?;
425 let store = self.open_store()?;
426
427 let options = CheckoutOptions {
428 paths: None,
429 force: false,
430 observer: None,
431 workspace_dir: None,
432 include_large: false,
433 };
434
435 let stats = checkout_tree(&store, &self.ctx.crypto.vault, &void_cid, self.ctx.paths.root.as_std_path(), &options)?;
436
437 refs::write_head(&self.ctx.paths.void_dir, &crate::refs::HeadRef::Symbolic(name.to_string()))?;
439
440 Ok(stats)
441 }
442
443 pub fn push(&self) -> Result<PushResult> {
450 let backend = self.default_backend();
451 push_repo(PushOptions {
452 ctx: self.ctx.clone(),
453 commit_cid: None,
454 backend,
455 timeout: std::time::Duration::from_secs(30),
456 pin: true,
457 backend_name: "local".to_string(),
458 full: false,
459 force: false,
460 observer: None,
461 remote: self.remote.clone(),
462 })
463 }
464
465 pub fn push_with_options(
467 &self,
468 full: bool,
469 force: bool,
470 observer: Option<Arc<dyn crate::support::events::VoidObserver>>,
471 ) -> Result<PushResult> {
472 let backend = self.default_backend();
473 push_repo(PushOptions {
474 ctx: self.ctx.clone(),
475 commit_cid: None,
476 backend,
477 timeout: std::time::Duration::from_secs(30),
478 pin: true,
479 backend_name: "local".to_string(),
480 full,
481 force,
482 observer,
483 remote: self.remote.clone(),
484 })
485 }
486
487 pub fn pull(&self, commit_cid: &str) -> Result<PullResult> {
491 let backend = self.default_backend();
492 pull_repo(PullOptions {
493 ctx: self.ctx.clone(),
494 commit_cid: commit_cid.to_string(),
495 backend,
496 timeout: std::time::Duration::from_secs(30),
497 mode: CloneMode::Full,
498 observer: None,
499 remote: self.remote.clone(),
500 })
501 }
502
503 pub fn pull_with_options(
505 &self,
506 commit_cid: &str,
507 mode: CloneMode,
508 observer: Option<Arc<dyn crate::support::events::VoidObserver>>,
509 ) -> Result<PullResult> {
510 let backend = self.default_backend();
511 pull_repo(PullOptions {
512 ctx: self.ctx.clone(),
513 commit_cid: commit_cid.to_string(),
514 backend,
515 timeout: std::time::Duration::from_secs(30),
516 mode,
517 observer,
518 remote: self.remote.clone(),
519 })
520 }
521
522 pub fn has_remote(&self) -> bool {
524 self.remote.is_some()
525 }
526
527 pub fn add_unixfs(&self, path: &std::path::Path) -> Result<crate::unixfs::AddResult> {
534 let store = self.open_store()?;
535 let adapter = crate::unixfs::FsStoreAdapter(&store);
536 crate::unixfs::add_directory(&adapter, path)
537 }
538
539 pub fn add_unixfs_bytes(&self, data: &[u8]) -> Result<crate::unixfs::FileAddResult> {
543 let store = self.open_store()?;
544 let adapter = crate::unixfs::FsStoreAdapter(&store);
545 crate::unixfs::add_bytes(&adapter, data)
546 }
547
548 pub fn cat_unixfs(&self, cid: &cid::Cid) -> Result<Vec<u8>> {
552 let store = self.open_store()?;
553 let adapter = crate::unixfs::FsStoreAdapter(&store);
554 crate::unixfs::cat(&adapter, cid)
555 }
556
557 fn default_backend(&self) -> IpfsBackend {
558 if let Some(ref ipfs) = self.ctx.network.ipfs {
559 if let Some(ref api) = ipfs.kubo_api {
560 return IpfsBackend::Kubo { api: api.clone() };
561 }
562 if let Some(ref gw) = ipfs.gateway {
563 return IpfsBackend::Gateway { base: gw.clone() };
564 }
565 }
566 IpfsBackend::Kubo {
567 api: "http://127.0.0.1:5001".to_string(),
568 }
569 }
570
571 pub fn vault(&self) -> &std::sync::Arc<crate::crypto::KeyVault> {
578 &self.ctx.crypto.vault
579 }
580
581 pub fn void_dir(&self) -> &camino::Utf8Path {
583 &self.ctx.paths.void_dir
584 }
585
586 pub fn root(&self) -> &camino::Utf8Path {
588 &self.ctx.paths.root
589 }
590
591 pub fn store(&self) -> Result<FsStore> {
593 self.ctx.open_store()
594 }
595
596 pub fn context(&self) -> &VoidContext {
602 &self.ctx
603 }
604
605 fn resolve_ref(&self, refstr: &str) -> Result<crate::crypto::CommitCid> {
610 if refstr == "HEAD" {
611 refs::resolve_head(&self.ctx.paths.void_dir)?
612 .ok_or_else(|| VoidError::NotFound("no HEAD commit".into()))
613 } else if let Some(branch_cid) = refs::read_branch(&self.ctx.paths.void_dir, refstr)? {
614 Ok(branch_cid)
615 } else {
616 let cid = crate::cid::parse(refstr)
618 .map_err(|_| VoidError::NotFound(format!("ref '{}' not found", refstr)))?;
619 Ok(crate::crypto::CommitCid::from_bytes(cid.to_bytes()))
620 }
621 }
622
623 fn open_store(&self) -> Result<FsStore> {
624 self.ctx.open_store()
625 }
626}