1use std::path::{Path, PathBuf};
2
3use crate::types::ResolveOptions;
4use git2::Repository;
5use libgrite_core::{
6 config::{
7 actor_dir, list_actors, load_actor_config, load_repo_config, load_signing_key,
8 repo_sled_path, save_actor_config, save_repo_config, RepoConfig,
9 },
10 lock::{LockCheckResult, LockPolicy},
11 signing::SigningKeyPair,
12 types::actor::ActorConfig,
13 types::event::Event,
14 types::ids::{generate_actor_id, id_to_hex},
15 GriteError, GriteStore, LockedStore,
16};
17use libgrite_git::{GitError, LockManager, SnapshotManager, SyncManager, WalManager};
18use libgrite_ipc::{DaemonLock, IpcClient};
19
20#[derive(Debug, Clone, Copy)]
22pub enum ActorSource {
23 DataDir,
24 Flag,
25 RepoDefault,
26 Auto,
27}
28
29impl ActorSource {
30 pub fn as_str(&self) -> &'static str {
31 match self {
32 ActorSource::DataDir => "env",
33 ActorSource::Flag => "flag",
34 ActorSource::RepoDefault => "repo_default",
35 ActorSource::Auto => "auto",
36 }
37 }
38}
39
40pub enum ExecutionMode {
42 Local,
44 Daemon { client: IpcClient, endpoint: String },
46 Blocked { lock: DaemonLock },
48}
49
50impl std::fmt::Debug for ExecutionMode {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 match self {
53 ExecutionMode::Local => write!(f, "Local"),
54 ExecutionMode::Daemon { endpoint, .. } => {
55 write!(f, "Daemon {{ endpoint: {} }}", endpoint)
56 }
57 ExecutionMode::Blocked { lock } => {
58 write!(
59 f,
60 "Blocked {{ pid: {}, expires_in: {}ms }}",
61 lock.pid,
62 lock.time_remaining_ms()
63 )
64 }
65 }
66 }
67}
68
69pub struct GriteContext {
71 pub git_dir: PathBuf,
72 pub actor_id: String,
73 pub actor_config: ActorConfig,
74 pub data_dir: PathBuf,
75 pub source: ActorSource,
76}
77
78impl Clone for GriteContext {
79 fn clone(&self) -> Self {
80 Self {
81 git_dir: self.git_dir.clone(),
82 actor_id: self.actor_id.clone(),
83 actor_config: self.actor_config.clone(),
84 data_dir: self.data_dir.clone(),
85 source: self.source,
86 }
87 }
88}
89
90impl GriteContext {
91 pub fn find_git_dir() -> Result<PathBuf, GriteError> {
93 Self::find_git_dir_at(std::env::current_dir()?)
94 }
95
96 pub fn find_git_dir_at(path: impl AsRef<Path>) -> Result<PathBuf, GriteError> {
97 let repo = Repository::discover(path.as_ref()).map_err(|_| {
98 GriteError::NotFound("Not a git repository (or any parent)".to_string())
99 })?;
100
101 Ok(repo.commondir().to_path_buf())
102 }
103
104 #[cfg(test)]
106 pub fn is_worktree() -> Result<bool, GriteError> {
107 Self::is_worktree_at(std::env::current_dir()?)
108 }
109
110 #[cfg(test)]
111 pub fn is_worktree_at(path: impl AsRef<Path>) -> Result<bool, GriteError> {
112 let repo = Repository::discover(path.as_ref()).map_err(|_| {
113 GriteError::NotFound("Not a git repository (or any parent)".to_string())
114 })?;
115
116 Ok(repo.path() != repo.commondir())
117 }
118
119 pub fn resolve(opts: &ResolveOptions) -> Result<Self, GriteError> {
121 let git_dir = Self::find_git_dir()?;
122
123 if let Some(ref data_dir) = opts.data_dir {
125 let config = load_actor_config(data_dir)?;
126 return Ok(Self {
127 git_dir,
128 actor_id: config.actor_id.clone(),
129 actor_config: config,
130 data_dir: data_dir.clone(),
131 source: ActorSource::DataDir,
132 });
133 }
134
135 if let Ok(grit_home) = std::env::var("GRITE_HOME") {
136 let data_dir = PathBuf::from(grit_home);
137 let config = load_actor_config(&data_dir)?;
138 return Ok(Self {
139 git_dir,
140 actor_id: config.actor_id.clone(),
141 actor_config: config,
142 data_dir,
143 source: ActorSource::DataDir,
144 });
145 }
146
147 if let Some(ref actor_id) = opts.actor {
149 let data_dir = actor_dir(&git_dir, actor_id);
150 let config = load_actor_config(&data_dir)?;
151 return Ok(Self {
152 git_dir,
153 actor_id: config.actor_id.clone(),
154 actor_config: config,
155 data_dir,
156 source: ActorSource::Flag,
157 });
158 }
159
160 if let Some(repo_config) = load_repo_config(&git_dir)? {
162 if let Some(ref default_actor) = repo_config.default_actor {
163 let data_dir = actor_dir(&git_dir, default_actor);
164 if let Ok(config) = load_actor_config(&data_dir) {
165 return Ok(Self {
166 git_dir,
167 actor_id: config.actor_id.clone(),
168 actor_config: config,
169 data_dir,
170 source: ActorSource::RepoDefault,
171 });
172 }
173 }
174 }
175
176 let actors = list_actors(&git_dir)?;
178 if let Some(first_actor) = actors.first() {
179 let data_dir = actor_dir(&git_dir, &first_actor.actor_id);
180 return Ok(Self {
181 git_dir,
182 actor_id: first_actor.actor_id.clone(),
183 actor_config: first_actor.clone(),
184 data_dir,
185 source: ActorSource::Auto,
186 });
187 }
188
189 let actor_id = generate_actor_id();
191 let actor_id_hex = id_to_hex(&actor_id);
192 let data_dir = actor_dir(&git_dir, &actor_id_hex);
193 let config = ActorConfig::new(actor_id, None);
194
195 save_actor_config(&data_dir, &config)?;
197
198 let repo_config = RepoConfig {
200 default_actor: Some(actor_id_hex.clone()),
201 ..Default::default()
202 };
203 save_repo_config(&git_dir, &repo_config)?;
204
205 Ok(Self {
206 git_dir,
207 actor_id: actor_id_hex,
208 actor_config: config,
209 data_dir,
210 source: ActorSource::Auto,
211 })
212 }
213
214 pub fn open_store(&self) -> Result<LockedStore, GriteError> {
216 GriteStore::open_locked(&repo_sled_path(&self.git_dir))
217 }
218
219 pub fn sled_path(&self) -> PathBuf {
221 repo_sled_path(&self.git_dir)
222 }
223
224 pub fn open_wal(&self) -> Result<WalManager, GitError> {
226 WalManager::open(&self.git_dir)
227 }
228
229 pub fn open_snapshot(&self) -> Result<SnapshotManager, GitError> {
231 SnapshotManager::open(&self.git_dir)
232 }
233
234 pub fn open_sync(&self) -> Result<SyncManager, GitError> {
236 SyncManager::open(&self.git_dir)
237 }
238
239 pub fn open_lock_manager(&self) -> Result<LockManager, GitError> {
241 LockManager::open(&self.git_dir)
242 }
243
244 pub fn get_lock_policy(&self) -> LockPolicy {
246 load_repo_config(&self.git_dir)
247 .ok()
248 .flatten()
249 .map(|c| c.get_lock_policy())
250 .unwrap_or(LockPolicy::Warn)
251 }
252
253 pub fn check_lock(&self, resource: &str) -> Result<LockCheckResult, GriteError> {
255 let policy = self.get_lock_policy();
256 if policy == LockPolicy::Off {
257 return Ok(LockCheckResult::Clear);
258 }
259
260 let lock_manager = self.open_lock_manager()?;
261
262 let result = lock_manager.check_conflicts(resource, &self.actor_id, policy)?;
263
264 if let LockCheckResult::Blocked(ref conflicts) = result {
265 let conflict_desc: Vec<String> = conflicts
266 .iter()
267 .map(|l| {
268 format!(
269 "{} (owned by {}, expires in {}s)",
270 l.resource,
271 l.owner,
272 l.time_remaining_ms() / 1000
273 )
274 })
275 .collect();
276 return Err(GriteError::Conflict(format!(
277 "Blocked by lock policy: {}",
278 conflict_desc.join(", ")
279 )));
280 }
281
282 Ok(result)
283 }
284
285 pub fn repo_root(&self) -> PathBuf {
287 self.git_dir.parent().unwrap_or(&self.git_dir).to_path_buf()
288 }
289
290 pub fn load_signing_key(&self) -> Option<SigningKeyPair> {
292 load_signing_key(&self.git_dir, &self.actor_id)
293 .and_then(|seed_hex| SigningKeyPair::from_seed_hex(&seed_hex).ok())
294 }
295
296 pub fn sign_event(&self, mut event: Event) -> Event {
298 if let Some(keypair) = self.load_signing_key() {
299 event.sig = Some(keypair.sign_event(&event));
300 }
301 event
302 }
303
304 pub fn execution_mode(&self, no_daemon: bool) -> ExecutionMode {
306 if no_daemon {
307 return ExecutionMode::Local;
308 }
309
310 match DaemonLock::read(&self.git_dir.join("grite")) {
311 Ok(Some(lock)) => {
312 if lock.is_expired() {
313 return ExecutionMode::Local;
314 }
315
316 match IpcClient::connect(&lock.ipc_endpoint) {
317 Ok(client) => ExecutionMode::Daemon {
318 endpoint: lock.ipc_endpoint.clone(),
319 client,
320 },
321 Err(_) => ExecutionMode::Blocked { lock },
322 }
323 }
324 Ok(None) => ExecutionMode::Local,
325 Err(_) => ExecutionMode::Local,
326 }
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use std::process::Command;
334 use tempfile::TempDir;
335
336 fn git(args: &[&str], dir: &std::path::Path) -> bool {
337 Command::new("git")
338 .args(args)
339 .current_dir(dir)
340 .output()
341 .map(|o| o.status.success())
342 .unwrap_or(false)
343 }
344
345 #[test]
346 fn test_find_git_dir_normal_repo() {
347 let temp = TempDir::new().unwrap();
348 assert!(git(&["init"], temp.path()));
349
350 let git_dir = GriteContext::find_git_dir_at(temp.path()).unwrap();
351 assert_eq!(
352 git_dir.canonicalize().unwrap(),
353 temp.path().join(".git").canonicalize().unwrap()
354 );
355 }
356
357 #[test]
358 fn test_find_git_dir_worktree() {
359 use git2::Repository;
360
361 let temp = TempDir::new().unwrap();
362 let main_repo = temp.path().join("main");
363 let worktree_path = temp.path().join("feature");
364 std::fs::create_dir_all(&main_repo).unwrap();
365
366 assert!(git(&["init"], &main_repo));
367 assert!(git(&["config", "user.email", "test@test.com"], &main_repo));
368 assert!(git(&["config", "user.name", "Test"], &main_repo));
369 assert!(git(&["commit", "--allow-empty", "-m", "init"], &main_repo));
370
371 assert!(git(
372 &[
373 "worktree",
374 "add",
375 worktree_path.to_str().unwrap(),
376 "-b",
377 "feature"
378 ],
379 &main_repo
380 ));
381
382 let git_file = worktree_path.join(".git");
383 assert!(
384 git_file.is_file(),
385 ".git should be a file in worktree, not a directory"
386 );
387
388 let repo =
389 Repository::discover(&worktree_path).expect("Should discover repo from worktree");
390
391 let commondir = repo.commondir();
392 let expected_commondir = main_repo.join(".git").canonicalize().unwrap();
393 let actual_commondir = commondir.canonicalize().unwrap();
394 assert_eq!(actual_commondir, expected_commondir);
395
396 assert_ne!(
397 repo.path(),
398 repo.commondir(),
399 "In worktree, path() != commondir()"
400 );
401 }
402
403 #[test]
404 fn test_is_worktree_main_repo() {
405 let temp = TempDir::new().unwrap();
406 assert!(git(&["init"], temp.path()));
407
408 assert!(!GriteContext::is_worktree_at(temp.path()).unwrap());
409 }
410
411 #[test]
412 fn test_find_git_dir_subdirectory() {
413 let temp = TempDir::new().unwrap();
414 assert!(git(&["init"], temp.path()));
415
416 let subdir = temp.path().join("src").join("deep");
417 std::fs::create_dir_all(&subdir).unwrap();
418
419 let git_dir = GriteContext::find_git_dir_at(&subdir).unwrap();
420 assert_eq!(
421 git_dir.canonicalize().unwrap(),
422 temp.path().join(".git").canonicalize().unwrap()
423 );
424 }
425}