1pub mod fallback;
10
11use anyhow::{Context, Result};
12use std::path::Path;
13use std::sync::Arc;
14use tracing::info;
15
16use super::provider::{Provider, ProviderManager, ProviderType};
17use super::vcs::VcsBackend;
18use super::vcs_lore::LoreBackend;
19
20pub struct RepositoryApi {
22 nap_home: std::path::PathBuf,
23 provider_manager: ProviderManager,
24}
25
26impl RepositoryApi {
27 pub fn new(nap_home: &Path) -> Result<Self> {
29 std::fs::create_dir_all(nap_home).with_context(|| {
31 format!(
32 "Failed to create NAP home directory at '{}'. \
33 Check permissions and disk space.",
34 nap_home.display()
35 )
36 })?;
37
38 let mut provider_manager = ProviderManager::new(nap_home);
39
40 provider_manager
42 .load_configured_provider()
43 .with_context(|| {
44 format!(
45 "Failed to load provider configuration from '{}'",
46 nap_home.join("provider.toml").display()
47 )
48 })?;
49
50 Ok(Self {
51 nap_home: nap_home.to_path_buf(),
52 provider_manager,
53 })
54 }
55
56 pub async fn initialize_provider_selection(&mut self) -> Result<ProviderType> {
58 info!("Initializing provider selection");
59
60 if let Some(provider) = self.provider_manager.active_provider() {
62 info!("Provider already configured: {}", provider.name());
63 return Ok(provider.provider_type());
64 }
65
66 let provider_type = ProviderType::Local;
69
70 let factory = super::provider::ProviderFactory::new(&self.nap_home);
71 let provider = factory.create_provider(provider_type)?;
72
73 self.provider_manager.set_active_provider(provider.clone());
74 self.provider_manager
75 .save_provider_config(provider.as_ref())?;
76
77 info!("Selected provider: {}", provider_type.as_str());
78 Ok(provider_type)
79 }
80
81 pub async fn ensure_provider_ready(&self) -> Result<()> {
83 if let Some(provider) = self.provider_manager.active_provider() {
84 provider.ensure_ready().await?;
85 Ok(())
86 } else {
87 anyhow::bail!(
88 "No active provider configured. Call initialize_provider_selection() first."
89 );
90 }
91 }
92
93 pub async fn create_repository(
95 &self,
96 repo_id: &str,
97 workspace_id: Option<&str>,
98 ) -> Result<RepositoryHandle> {
99 info!("Creating repository: {}", repo_id);
100
101 let provider = self.provider_manager.active_provider().context(
102 "No active provider configured. Call initialize_provider_selection() first.",
103 )?;
104
105 provider.ensure_ready().await.with_context(|| {
107 format!(
108 "Failed to ensure provider '{}' is ready for repository creation",
109 provider.name()
110 )
111 })?;
112
113 let lore_url_base = provider.lore_url_base().with_context(|| {
115 format!(
116 "Failed to get Lore URL base from provider '{}'",
117 provider.name()
118 )
119 })?;
120 let workspace = workspace_id.unwrap_or(provider.workspace_id());
121
122 let lore_backend = LoreBackend::from_provider(&lore_url_base, workspace);
124
125 let workspace_root = &self.nap_home;
127 let repo_path = workspace_root.join(repo_id);
128
129 if let Some(parent) = repo_path.parent() {
131 std::fs::create_dir_all(parent).with_context(|| {
132 format!(
133 "Failed to create parent directory for repository at '{}'",
134 repo_path.display()
135 )
136 })?;
137 }
138
139 let repo_existed_before = repo_path.exists();
141
142 match lore_backend.init(&repo_path) {
143 Ok(()) => {
144 info!("Repository created: {}", repo_id);
145 Ok(RepositoryHandle {
146 id: repo_id.to_string(),
147 workspace_id: workspace.to_string(),
148 path: repo_path,
149 lore_url_base,
150 })
151 }
152 Err(e) => {
153 if repo_path.exists() && !repo_existed_before {
155 tracing::warn!(
156 repo_id,
157 path = %repo_path.display(),
158 "Repository creation failed, cleaning up partial repository at '{}'",
159 repo_path.display()
160 );
161 if let Err(cleanup_err) = std::fs::remove_dir_all(&repo_path) {
162 tracing::error!(
163 path = %repo_path.display(),
164 error = %cleanup_err,
165 "Failed to clean up partial repository directory after creation failure"
166 );
167 }
168 }
169 Err(anyhow::anyhow!(
170 "Failed to create repository '{}': {}. \
171 The operation has been rolled back and no partial state remains.",
172 repo_id,
173 e
174 ))
175 }
176 }
177 }
178
179 pub async fn open_repository(&self, repo_id: &str) -> Result<RepositoryHandle> {
181 info!("Opening repository: {}", repo_id);
182
183 let provider = self.provider_manager.active_provider().context(
184 "No active provider configured. Call initialize_provider_selection() first.",
185 )?;
186
187 let lore_url_base = provider.lore_url_base().with_context(|| {
188 format!(
189 "Failed to get Lore URL base from provider '{}'",
190 provider.name()
191 )
192 })?;
193 let workspace_id = provider.workspace_id();
194
195 let repo_path = self.nap_home.join(repo_id);
196
197 if !repo_path.exists() {
198 anyhow::bail!(
199 "Repository '{}' not found at '{}'. \
200 Verify the repository ID and ensure it has been created.",
201 repo_id,
202 repo_path.display()
203 );
204 }
205
206 info!("Repository opened: {}", repo_id);
207
208 Ok(RepositoryHandle {
209 id: repo_id.to_string(),
210 workspace_id: workspace_id.to_string(),
211 path: repo_path,
212 lore_url_base,
213 })
214 }
215
216 pub async fn publish(&self, repo_handle: &RepositoryHandle, message: &str) -> Result<String> {
218 info!("Publishing changes to repository: {}", repo_handle.id);
219
220 let provider = self.provider_manager.active_provider().context(
221 "No active provider configured. Call initialize_provider_selection() first.",
222 )?;
223
224 let workspace_id = provider.workspace_id();
225 let lore_backend = LoreBackend::from_provider(&repo_handle.lore_url_base, workspace_id);
226
227 let commit_hash = lore_backend
229 .commit(&repo_handle.path, message, "nap")
230 .with_context(|| {
231 format!(
232 "Failed to commit changes to repository '{}' at '{}'. \
233 Check repository state and permissions.",
234 repo_handle.id,
235 repo_handle.path.display()
236 )
237 })?;
238
239 match provider.provider_type() {
241 ProviderType::Local => {
242 info!("Published locally: {}", commit_hash);
244 }
245 ProviderType::PortalsCloud | ProviderType::Remote => {
246 match lore_backend.push(&repo_handle.path, None, None) {
249 Ok(_) => {
250 info!("Published and synchronized: {}", commit_hash);
251 }
252 Err(push_err) => {
253 tracing::warn!(
255 repo_id = %repo_handle.id,
256 commit_hash = %commit_hash,
257 "Push failed, rolling back commit to keep local state consistent"
258 );
259 let rollback_result = lore_backend.revert(&repo_handle.path, &commit_hash);
260 if let Err(revert_err) = rollback_result {
261 tracing::error!(
262 repo_id = %repo_handle.id,
263 commit_hash = %commit_hash,
264 error = %revert_err,
265 "Failed to rollback commit after push failure - repository may be in inconsistent state"
266 );
267 return Err(anyhow::anyhow!(
268 "Failed to push changes to remote for repository '{}'. \
269 Attempted to rollback commit '{}' but failed. \
270 Repository may be in inconsistent state. \
271 Recovery options: \
272 1. Manually revert the commit: lore revert {} \
273 2. Reset to previous state: lore reset --hard HEAD~1 \
274 3. Check repository status: lore status \
275 Original push error: {}. Rollback error: {}",
276 repo_handle.id,
277 commit_hash,
278 commit_hash,
279 push_err,
280 revert_err
281 ));
282 }
283 return Err(anyhow::anyhow!(
284 "Failed to push changes to remote for repository '{}'. \
285 The commit has been successfully rolled back. \
286 Original error: {}",
287 repo_handle.id,
288 push_err
289 ));
290 }
291 }
292 }
293 }
294
295 Ok(commit_hash)
296 }
297
298 pub async fn history(
300 &self,
301 repo_handle: &RepositoryHandle,
302 limit: usize,
303 ) -> Result<Vec<CommitInfo>> {
304 info!("Getting history for repository: {}", repo_handle.id);
305
306 let provider = self.provider_manager.active_provider().context(
307 "No active provider configured. Call initialize_provider_selection() first.",
308 )?;
309
310 let workspace_id = provider.workspace_id();
311 let lore_backend = LoreBackend::from_provider(&repo_handle.lore_url_base, workspace_id);
312
313 let commits = lore_backend
314 .log(&repo_handle.path, None, limit)
315 .with_context(|| {
316 format!(
317 "Failed to get history for repository '{}' at '{}'. \
318 Check repository state and Lore server connectivity.",
319 repo_handle.id,
320 repo_handle.path.display()
321 )
322 })?;
323
324 Ok(commits)
325 }
326
327 pub async fn create_branch(
329 &self,
330 repo_handle: &RepositoryHandle,
331 branch_name: &str,
332 ) -> Result<()> {
333 info!(
334 "Creating branch: {} in repository: {}",
335 branch_name, repo_handle.id
336 );
337
338 let provider = self.provider_manager.active_provider().context(
339 "No active provider configured. Call initialize_provider_selection() first.",
340 )?;
341
342 let workspace_id = provider.workspace_id();
343 let lore_backend = LoreBackend::from_provider(&repo_handle.lore_url_base, workspace_id);
344
345 lore_backend
346 .create_branch(&repo_handle.path, branch_name)
347 .with_context(|| {
348 format!(
349 "Failed to create branch '{}' in repository '{}'. \
350 Verify branch name is valid and repository is in a valid state.",
351 branch_name, repo_handle.id
352 )
353 })?;
354
355 Ok(())
356 }
357
358 pub async fn switch_branch(
360 &self,
361 repo_handle: &RepositoryHandle,
362 branch_name: &str,
363 ) -> Result<()> {
364 info!(
365 "Switching to branch: {} in repository: {}",
366 branch_name, repo_handle.id
367 );
368
369 let provider = self.provider_manager.active_provider().context(
370 "No active provider configured. Call initialize_provider_selection() first.",
371 )?;
372
373 let workspace_id = provider.workspace_id();
374 let lore_backend = LoreBackend::from_provider(&repo_handle.lore_url_base, workspace_id);
375
376 lore_backend
377 .switch_branch(&repo_handle.path, branch_name)
378 .with_context(|| {
379 format!(
380 "Failed to switch to branch '{}' in repository '{}'. \
381 Verify branch exists and repository is in a clean state.",
382 branch_name, repo_handle.id
383 )
384 })?;
385
386 Ok(())
387 }
388
389 pub async fn list_branches(&self, repo_handle: &RepositoryHandle) -> Result<Vec<String>> {
391 info!("Listing branches in repository: {}", repo_handle.id);
392
393 let provider = self.provider_manager.active_provider().context(
394 "No active provider configured. Call initialize_provider_selection() first.",
395 )?;
396
397 let workspace_id = provider.workspace_id();
398 let lore_backend = LoreBackend::from_provider(&repo_handle.lore_url_base, workspace_id);
399
400 let branches = lore_backend
401 .list_branches(&repo_handle.path)
402 .with_context(|| {
403 format!(
404 "Failed to list branches in repository '{}'. \
405 Check repository state and Lore server connectivity.",
406 repo_handle.id
407 )
408 })?;
409
410 Ok(branches)
411 }
412
413 pub async fn sync(&self, repo_handle: &RepositoryHandle) -> Result<()> {
415 info!("Synchronizing repository: {}", repo_handle.id);
416
417 let provider = self.provider_manager.active_provider().context(
418 "No active provider configured. Call initialize_provider_selection() first.",
419 )?;
420
421 match provider.provider_type() {
422 ProviderType::Local => {
423 info!("Synchronization not needed for local provider");
424 Ok(())
425 }
426 ProviderType::PortalsCloud | ProviderType::Remote => {
427 let workspace_id = provider.workspace_id();
428 let lore_backend =
429 LoreBackend::from_provider(&repo_handle.lore_url_base, workspace_id);
430
431 let head_before_pull = lore_backend.head_hash(&repo_handle.path).ok();
433
434 lore_backend
435 .pull(&repo_handle.path, None, None)
436 .with_context(|| {
437 format!(
438 "Failed to pull changes for repository '{}'. \
439 Check network connectivity and remote server status.",
440 repo_handle.id
441 )
442 })?;
443
444 match lore_backend.push(&repo_handle.path, None, None) {
445 Ok(_) => {
446 info!("Repository synchronized");
447 Ok(())
448 }
449 Err(push_err) => {
450 if let Some(ref old_head) = head_before_pull {
452 tracing::warn!(
453 repo_id = %repo_handle.id,
454 old_head = %old_head,
455 "Push failed after pull, attempting to rollback to previous state"
456 );
457 let rollback_result = lore_backend.revert(&repo_handle.path, old_head);
458 if let Err(revert_err) = rollback_result {
459 tracing::error!(
460 repo_id = %repo_handle.id,
461 old_head = %old_head,
462 error = %revert_err,
463 "Failed to rollback sync operation - repository may be in inconsistent state"
464 );
465 return Err(anyhow::anyhow!(
466 "Failed to push changes for repository '{}'. \
467 Pull was attempted but push failed. \
468 Attempted to rollback to previous state '{}' but failed. \
469 Repository may be in inconsistent state. \
470 Recovery options: \
471 1. Manually reset to previous state: lore reset --hard {} \
472 2. Check repository status: lore status \
473 3. Force sync from remote: lore pull --force \
474 Original push error: {}. Rollback error: {}",
475 repo_handle.id,
476 old_head,
477 old_head,
478 push_err,
479 revert_err
480 ));
481 }
482 return Err(anyhow::anyhow!(
483 "Failed to push changes for repository '{}'. \
484 Pull was attempted but push failed. \
485 Successfully rolled back to previous state '{}'. \
486 Original error: {}",
487 repo_handle.id,
488 old_head,
489 push_err
490 ));
491 }
492 Err(anyhow::anyhow!(
493 "Failed to push changes for repository '{}'. \
494 Pull was attempted but push failed. \
495 Could not rollback (no previous state recorded). \
496 Original error: {}",
497 repo_handle.id,
498 push_err
499 ))
500 }
501 }
502 }
503 }
504 }
505
506 pub async fn delete_repository(&self, repo_handle: &RepositoryHandle) -> Result<()> {
508 info!("Deleting repository: {}", repo_handle.id);
509
510 std::fs::remove_dir_all(&repo_handle.path).with_context(|| {
512 format!(
513 "Failed to remove repository directory at '{}'. \
514 Check file permissions and ensure no processes are using the repository.",
515 repo_handle.path.display()
516 )
517 })?;
518
519 info!("Repository deleted: {}", repo_handle.id);
520 Ok(())
521 }
522
523 pub fn active_provider(&self) -> Option<&Arc<dyn Provider>> {
525 self.provider_manager.active_provider()
526 }
527
528 pub fn provider_manager_mut(&mut self) -> &mut ProviderManager {
530 &mut self.provider_manager
531 }
532
533 pub fn provider_manager(&self) -> &ProviderManager {
535 &self.provider_manager
536 }
537
538 pub async fn provider_status(&self) -> Result<super::provider::ProviderStatus> {
540 let provider = self
541 .provider_manager
542 .active_provider()
543 .context("No active provider configured")?;
544
545 provider.status().await
546 }
547}
548
549#[derive(Debug, Clone)]
551pub struct RepositoryHandle {
552 pub id: String,
553 pub workspace_id: String,
554 pub path: std::path::PathBuf,
555 pub lore_url_base: String,
556}
557
558pub use super::vcs::CommitInfo;
562
563pub use fallback::{FallbackHandler, FallbackResult, FallbackStrategy, RepositoryApiFallback};
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569 use tempfile::TempDir;
570
571 #[test]
572 fn test_repository_api_creation() {
573 let temp_dir = TempDir::new().unwrap();
574 let api = RepositoryApi::new(temp_dir.path()).unwrap();
575 assert_eq!(api.nap_home, temp_dir.path());
576 }
577
578 #[test]
579 fn test_repository_handle() {
580 let handle = RepositoryHandle {
581 id: "test-repo".to_string(),
582 workspace_id: "default".to_string(),
583 path: std::path::PathBuf::from("/tmp/test-repo"),
584 lore_url_base: "lore://localhost:41337".to_string(),
585 };
586
587 assert_eq!(handle.id, "test-repo");
588 assert_eq!(handle.workspace_id, "default");
589 }
590}