1#![warn(missing_docs)]
7#![warn(clippy::pedantic)]
8
9pub mod engine;
10pub mod error;
11pub mod filter;
12pub mod ops;
13pub mod repo_source;
14pub mod sync_lifecycle;
15pub mod tracking;
16
17use std::{
18 path::{Path, PathBuf},
19 sync::OnceLock,
20 time::Duration
21};
22
23pub use error::{GitError, classify_git_error};
24use omnifuse_core::{Backend, InitResult, RemoteRefresh, RemoteRefreshResult, SyncResult};
25
26use crate::{
27 sync_lifecycle::{GitInit, GitSync, GitSyncLifecycle},
28 tracking::GitTrackingRules
29};
30
31#[derive(Debug, Clone)]
33pub struct GitConfig {
34 pub source: String,
36 pub branch: String,
38 pub max_push_retries: u32,
40 pub poll_interval_secs: u64,
42 pub local_dir: PathBuf
44}
45
46impl Default for GitConfig {
47 fn default() -> Self {
48 Self {
49 source: String::new(),
50 branch: "main".to_string(),
51 max_push_retries: 3,
52 poll_interval_secs: 30,
53 local_dir: PathBuf::new()
54 }
55 }
56}
57
58#[derive(Debug)]
63pub struct GitBackend {
64 config: GitConfig,
66 lifecycle: OnceLock<GitSyncLifecycle>
68}
69
70impl GitBackend {
71 #[must_use]
73 pub const fn new(config: GitConfig) -> Self {
74 Self {
75 config,
76 lifecycle: OnceLock::new()
77 }
78 }
79
80 fn lifecycle(&self) -> anyhow::Result<&GitSyncLifecycle> {
86 self.lifecycle.get().ok_or_else(|| GitError::NotInitialized.into())
87 }
88}
89
90impl Backend for GitBackend {
91 async fn init(&self, local_dir: &Path) -> anyhow::Result<InitResult> {
92 let (lifecycle, init) = GitSyncLifecycle::open(self.config.clone(), local_dir).await?;
93 let _ = self.lifecycle.set(lifecycle);
94 Ok(map_init(init))
95 }
96
97 async fn sync(&self, dirty_files: &[PathBuf]) -> anyhow::Result<SyncResult> {
98 match self.lifecycle()?.sync_local(dirty_files).await? {
99 GitSync::Success { synced_files } => Ok(SyncResult::Success { synced_files }),
100 GitSync::Conflict { files } => Ok(SyncResult::Conflict {
101 synced_files: 0,
102 conflict_files: files
103 }),
104 GitSync::Offline => Ok(SyncResult::Offline)
105 }
106 }
107
108 async fn refresh_remote(&self, request: RemoteRefresh<'_>) -> anyhow::Result<RemoteRefreshResult> {
109 self.lifecycle()?.refresh_remote_protected(request).await
110 }
111
112 fn should_track(&self, path: &Path) -> bool {
113 if GitTrackingRules::contains_git_directory(path) {
114 return false;
115 }
116
117 self
118 .lifecycle
119 .get()
120 .is_none_or(|lifecycle| lifecycle.should_track(path))
121 }
122
123 fn poll_interval(&self) -> Duration {
124 Duration::from_secs(self.config.poll_interval_secs)
125 }
126
127 async fn is_online(&self) -> bool {
128 match self.lifecycle() {
129 Ok(lifecycle) => lifecycle.is_online().await,
130 Err(_) => false
131 }
132 }
133
134 fn name(&self) -> &'static str {
135 "git"
136 }
137
138 fn classify_error(&self, error: &anyhow::Error) -> omnifuse_core::ErrorKind {
139 self.lifecycle.get().map_or_else(
140 || classify_git_error(error).unwrap_or(omnifuse_core::ErrorKind::Internal),
141 |lifecycle| lifecycle.classify(error)
142 )
143 }
144}
145
146fn map_init(init: GitInit) -> InitResult {
147 match init {
148 GitInit::UpToDate => InitResult::UpToDate,
149 GitInit::Updated => InitResult::Updated,
150 GitInit::Conflicts { files } => InitResult::Conflicts { files },
151 GitInit::Offline => InitResult::Offline
152 }
153}