Skip to main content

omnifuse_git/
lib.rs

1//! omnifuse-git — Git backend for `OmniFuse`.
2//!
3//! Implements the `omnifuse_core::Backend` trait via git CLI.
4//! Ported from `SimpleGitFS`.
5
6#![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/// Git backend configuration.
32#[derive(Debug, Clone)]
33pub struct GitConfig {
34  /// Source: URL or local path.
35  pub source: String,
36  /// Branch.
37  pub branch: String,
38  /// Maximum number of push retries.
39  pub max_push_retries: u32,
40  /// Remote polling interval (seconds).
41  pub poll_interval_secs: u64,
42  /// Local working directory (clone target).
43  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/// Git backend for `OmniFuse`.
59///
60/// Implements the `Backend` trait: init -> clone/fetch, sync -> commit+push,
61/// refresh -> fetch+safe pull.
62#[derive(Debug)]
63pub struct GitBackend {
64  /// Configuration.
65  config: GitConfig,
66  /// Git lifecycle (initialized in `init`).
67  lifecycle: OnceLock<GitSyncLifecycle>
68}
69
70impl GitBackend {
71  /// Create a new git backend.
72  #[must_use]
73  pub const fn new(config: GitConfig) -> Self {
74    Self {
75      config,
76      lifecycle: OnceLock::new()
77    }
78  }
79
80  /// Get lifecycle (after initialization).
81  ///
82  /// # Errors
83  ///
84  /// Returns an error if the backend is not initialized.
85  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}