use crate::git::{ GitCommit, GitPullResult, GitPushResult, GitRemoteStatus, LastCommitInfo, ModifiedFile, PulseCommit, }; use super::expand_tilde; type VaultPathArg = String; type NotePathArg = String; type CommitHashArg = String; type CommitMessageArg = String; type ConflictStrategyArg = String; type RemoteUrlArg = String; type LocalPathArg = String; // ── Git commands (desktop) ────────────────────────────────────────────────── #[cfg(desktop)] #[tauri::command] pub fn get_file_history( vault_path: VaultPathArg, path: NotePathArg, ) -> Result, String> { let vault_path = expand_tilde(&vault_path); let path = expand_tilde(&path); crate::git::get_file_history(&vault_path, &path) } #[cfg(desktop)] #[tauri::command] pub fn get_modified_files(vault_path: VaultPathArg) -> Result, String> { let vault_path = expand_tilde(&vault_path); crate::git::get_modified_files(&vault_path) } #[cfg(desktop)] #[tauri::command] pub fn get_file_diff(vault_path: VaultPathArg, path: NotePathArg) -> Result { let vault_path = expand_tilde(&vault_path); let path = expand_tilde(&path); crate::git::get_file_diff(&vault_path, &path) } #[cfg(desktop)] #[tauri::command] pub fn get_file_diff_at_commit( vault_path: VaultPathArg, path: NotePathArg, commit_hash: CommitHashArg, ) -> Result { let vault_path = expand_tilde(&vault_path); let path = expand_tilde(&path); crate::git::get_file_diff_at_commit(&vault_path, &path, &commit_hash) } #[cfg(desktop)] #[tauri::command] pub fn get_vault_pulse( vault_path: VaultPathArg, limit: Option, skip: Option, ) -> Result, String> { let vault_path = expand_tilde(&vault_path); let limit = limit.unwrap_or(20); let skip = skip.unwrap_or(0); crate::git::get_vault_pulse(&vault_path, limit, skip) } #[cfg(desktop)] #[tauri::command] pub fn git_commit(vault_path: VaultPathArg, message: CommitMessageArg) -> Result { let vault_path = expand_tilde(&vault_path); crate::git::git_commit(&vault_path, &message) } #[cfg(desktop)] #[tauri::command] pub fn get_last_commit_info(vault_path: VaultPathArg) -> Result, String> { let vault_path = expand_tilde(&vault_path); crate::git::get_last_commit_info(&vault_path) } #[cfg(desktop)] #[tauri::command] pub async fn git_pull(vault_path: VaultPathArg) -> Result { let vault_path = expand_tilde(&vault_path).into_owned(); tokio::task::spawn_blocking(move || crate::git::git_pull(&vault_path)) .await .map_err(|e| format!("Task panicked: {e}"))? } #[cfg(desktop)] #[tauri::command] pub fn get_conflict_files(vault_path: VaultPathArg) -> Result, String> { let vault_path = expand_tilde(&vault_path); crate::git::get_conflict_files(&vault_path) } #[cfg(desktop)] #[tauri::command] pub fn get_conflict_mode(vault_path: VaultPathArg) -> String { let vault_path = expand_tilde(&vault_path); crate::git::get_conflict_mode(&vault_path) } #[cfg(desktop)] #[tauri::command] pub fn git_resolve_conflict( vault_path: VaultPathArg, file: NotePathArg, strategy: ConflictStrategyArg, ) -> Result<(), String> { let vault_path = expand_tilde(&vault_path); crate::git::git_resolve_conflict(&vault_path, &file, &strategy) } #[cfg(desktop)] #[tauri::command] pub fn git_commit_conflict_resolution(vault_path: VaultPathArg) -> Result { let vault_path = expand_tilde(&vault_path); crate::git::git_commit_conflict_resolution(&vault_path) } #[cfg(desktop)] #[tauri::command] pub async fn git_push(vault_path: VaultPathArg) -> Result { let vault_path = expand_tilde(&vault_path).into_owned(); tokio::task::spawn_blocking(move || crate::git::git_push(&vault_path)) .await .map_err(|e| format!("Task panicked: {e}"))? } #[cfg(desktop)] #[tauri::command] pub async fn git_remote_status(vault_path: VaultPathArg) -> Result { let vault_path = expand_tilde(&vault_path).into_owned(); tokio::task::spawn_blocking(move || crate::git::git_remote_status(&vault_path)) .await .map_err(|e| format!("Task panicked: {e}"))? } #[cfg(desktop)] #[tauri::command] pub fn git_discard_file( vault_path: VaultPathArg, relative_path: NotePathArg, ) -> Result<(), String> { let vault_path = expand_tilde(&vault_path); crate::git::discard_file_changes(&vault_path, &relative_path) } #[cfg(desktop)] #[tauri::command] pub fn is_git_repo(vault_path: VaultPathArg) -> bool { let vault_path = expand_tilde(&vault_path); std::path::Path::new(vault_path.as_ref()) .join(".git") .is_dir() } #[cfg(desktop)] #[tauri::command] pub fn init_git_repo(vault_path: VaultPathArg) -> Result<(), String> { let vault_path = expand_tilde(&vault_path); crate::git::init_repo(&vault_path) } #[cfg(desktop)] #[tauri::command] pub fn clone_repo(url: RemoteUrlArg, local_path: LocalPathArg) -> Result { let local_path = expand_tilde(&local_path); crate::git::clone_repo(&url, &local_path) } // ── Git commands (mobile stubs) ───────────────────────────────────────────── #[cfg(mobile)] #[tauri::command] pub fn get_file_history( _vault_path: VaultPathArg, _path: NotePathArg, ) -> Result, String> { Err("Git history is not available on mobile".into()) } #[cfg(mobile)] #[tauri::command] pub fn get_modified_files(_vault_path: VaultPathArg) -> Result, String> { Ok(vec![]) } #[cfg(mobile)] #[tauri::command] pub fn get_file_diff(_vault_path: VaultPathArg, _path: NotePathArg) -> Result { Err("Git diff is not available on mobile".into()) } #[cfg(mobile)] #[tauri::command] pub fn get_file_diff_at_commit( _vault_path: VaultPathArg, _path: NotePathArg, _commit_hash: CommitHashArg, ) -> Result { Err("Git diff is not available on mobile".into()) } #[cfg(mobile)] #[tauri::command] pub fn get_vault_pulse( _vault_path: VaultPathArg, _limit: Option, _skip: Option, ) -> Result, String> { Ok(vec![]) } #[cfg(mobile)] #[tauri::command] pub fn git_commit(_vault_path: VaultPathArg, _message: CommitMessageArg) -> Result { Err("Git commit is not available on mobile".into()) } #[cfg(mobile)] #[tauri::command] pub fn get_last_commit_info(_vault_path: VaultPathArg) -> Result, String> { Ok(None) } #[cfg(mobile)] #[tauri::command] pub async fn git_pull(_vault_path: VaultPathArg) -> Result { Err("Git pull is not available on mobile".into()) } #[cfg(mobile)] #[tauri::command] pub fn get_conflict_files(_vault_path: VaultPathArg) -> Result, String> { Ok(vec![]) } #[cfg(mobile)] #[tauri::command] pub fn get_conflict_mode(_vault_path: VaultPathArg) -> String { "none".to_string() } #[cfg(mobile)] #[tauri::command] pub fn git_resolve_conflict( _vault_path: VaultPathArg, _file: NotePathArg, _strategy: ConflictStrategyArg, ) -> Result<(), String> { Err("Git conflict resolution is not available on mobile".into()) } #[cfg(mobile)] #[tauri::command] pub fn git_commit_conflict_resolution(_vault_path: VaultPathArg) -> Result { Err("Git conflict resolution is not available on mobile".into()) } #[cfg(mobile)] #[tauri::command] pub async fn git_push(_vault_path: VaultPathArg) -> Result { Err("Git push is not available on mobile".into()) } #[cfg(mobile)] #[tauri::command] pub async fn git_remote_status(_vault_path: VaultPathArg) -> Result { Ok(GitRemoteStatus { branch: String::new(), has_remote: false, ahead: 0, behind: 0, }) } #[cfg(mobile)] #[tauri::command] pub fn git_discard_file( _vault_path: VaultPathArg, _relative_path: NotePathArg, ) -> Result<(), String> { Err("Git discard is not available on mobile".into()) } #[cfg(mobile)] #[tauri::command] pub fn is_git_repo(_vault_path: VaultPathArg) -> bool { false } #[cfg(mobile)] #[tauri::command] pub fn init_git_repo(_vault_path: VaultPathArg) -> Result<(), String> { Err("Git init is not available on mobile".into()) } #[cfg(mobile)] #[tauri::command] pub fn clone_repo(_url: RemoteUrlArg, _local_path: LocalPathArg) -> Result { Err("Git clone is not available on mobile".into()) } #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::TempDir; fn vault_path(dir: &TempDir) -> String { dir.path().to_string_lossy().into_owned() } fn note_path(dir: &TempDir, name: &str) -> String { dir.path().join(name).to_string_lossy().into_owned() } fn create_initialized_vault() -> (TempDir, String) { let dir = TempDir::new().unwrap(); fs::write(dir.path().join("note.md"), "# Note\n").unwrap(); let vault = vault_path(&dir); init_git_repo(vault.clone()).unwrap(); (dir, vault) } #[test] fn desktop_git_commands_route_to_git_backend() { let (dir, vault) = create_initialized_vault(); let note = note_path(&dir, "note.md"); assert!(is_git_repo(vault.clone())); fs::write(dir.path().join("note.md"), "# Updated\n").unwrap(); let modified = get_modified_files(vault.clone()).unwrap(); assert!(modified.iter().any(|file| file.relative_path == "note.md")); let diff = get_file_diff(vault.clone(), note.clone()).unwrap(); assert!(diff.contains("# Updated")); git_commit(vault.clone(), "Update note".to_string()).unwrap(); let history = get_file_history(vault.clone(), note.clone()).unwrap(); assert!(history.iter().any(|commit| commit.message == "Update note")); let last_commit = get_last_commit_info(vault.clone()).unwrap().unwrap(); assert!(!last_commit.short_hash.is_empty()); let commit_diff = get_file_diff_at_commit( vault.clone(), note.clone(), history.first().unwrap().hash.clone(), ) .unwrap(); assert!(commit_diff.contains("# Updated")); let pulse = get_vault_pulse(vault.clone(), Some(5), Some(0)).unwrap(); assert!(!pulse.is_empty()); fs::write(dir.path().join("note.md"), "# Discard me\n").unwrap(); git_discard_file(vault.clone(), "note.md".to_string()).unwrap(); assert_eq!( fs::read_to_string(dir.path().join("note.md")).unwrap(), "# Updated\n" ); assert!(get_conflict_files(vault.clone()).unwrap().is_empty()); assert_eq!(get_conflict_mode(vault.clone()), "none"); assert!( git_resolve_conflict(vault.clone(), "note.md".to_string(), "invalid".to_string(),) .is_err() ); } #[tokio::test] async fn desktop_remote_commands_report_no_remote() { let (_dir, vault) = create_initialized_vault(); let pull = git_pull(vault.clone()).await.unwrap(); assert_eq!(pull.status, "no_remote"); let push = git_push(vault.clone()).await.unwrap(); assert_eq!(push.status, "error"); let status = git_remote_status(vault.clone()).await.unwrap(); assert!(!status.has_remote); assert_eq!((status.ahead, status.behind), (0, 0)); } }