diff --git a/design/gs-git-init.pen b/design/gs-git-init.pen new file mode 100644 index 00000000..ddf305fe --- /dev/null +++ b/design/gs-git-init.pen @@ -0,0 +1 @@ +{"children":[{"type":"frame","id":"gs_git_init_after","name":"Getting Started Vault — Git Initialized","x":0,"y":0,"width":600,"height":"fit_content(200)","fill":"#FFFFFF","layout":"vertical","gap":16,"padding":[32,32],"children":[{"type":"text","id":"gs_title","content":"After: Getting Started vault created","fontFamily":"Inter","fontSize":20,"fontWeight":"700","fill":"#111111"},{"type":"text","id":"gs_desc","content":"When a new Getting Started vault is created, it is automatically initialized as a git repo with an initial commit containing all sample files.","fontFamily":"Inter","fontSize":14,"lineHeight":1.5,"fill":"#555555"},{"type":"frame","id":"gs_terminal","name":"Git Log Output","width":"fill_container","height":"fit_content(80)","fill":"#1E1E1E","cornerRadius":[8,8,8,8],"padding":[16,16],"layout":"vertical","gap":4,"children":[{"type":"text","id":"gs_prompt","content":"$ git log --oneline","fontFamily":"JetBrains Mono, monospace","fontSize":12,"fill":"#AAAAAA"},{"type":"text","id":"gs_log","content":"a1b2c3d Initial vault setup","fontFamily":"JetBrains Mono, monospace","fontSize":12,"fill":"#66FF66"}]},{"type":"frame","id":"gs_details","name":"Details","width":"fill_container","layout":"vertical","gap":8,"children":[{"type":"text","id":"gs_detail1","content":"git init + git add . + git commit","fontFamily":"Inter","fontSize":13,"fill":"#333333"},{"type":"text","id":"gs_detail2","content":"Fallback author: Laputa (if no global git config)","fontFamily":"Inter","fontSize":13,"fill":"#333333"},{"type":"text","id":"gs_detail3","content":"No UI changes — backend only (git.rs + getting_started.rs)","fontFamily":"Inter","fontSize":13,"fill":"#333333"}]}]}],"variables":{}} \ No newline at end of file diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index 481965dc..2ed743a0 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -12,6 +12,54 @@ pub struct GitCommit { pub date: i64, } +/// Initialize a new git repository, stage all files, and create an initial commit. +pub fn init_repo(path: &str) -> Result<(), String> { + let dir = Path::new(path); + + run_git(dir, &["init"])?; + ensure_author_config(dir)?; + run_git(dir, &["add", "."])?; + run_git(dir, &["commit", "-m", "Initial vault setup"])?; + + Ok(()) +} + +/// Run a git command in the given directory, returning an error on failure. +fn run_git(dir: &Path, args: &[&str]) -> Result<(), String> { + let output = Command::new("git") + .args(args) + .current_dir(dir) + .output() + .map_err(|e| format!("Failed to run git {}: {}", args[0], e))?; + + if output.status.success() { + return Ok(()); + } + + Err(format!( + "git {} failed: {}", + args[0], + String::from_utf8_lossy(&output.stderr) + )) +} + +/// Set local user.name and user.email if not already configured. +fn ensure_author_config(dir: &Path) -> Result<(), String> { + for (key, fallback) in [("user.name", "Laputa"), ("user.email", "vault@laputa.app")] { + let check = Command::new("git") + .args(["config", key]) + .current_dir(dir) + .output() + .map_err(|e| format!("Failed to check git config: {}", e))?; + + let value = String::from_utf8_lossy(&check.stdout); + if !check.status.success() || value.trim().is_empty() { + run_git(dir, &["config", key, fallback])?; + } + } + Ok(()) +} + /// Get git log history for a specific file in the vault. pub fn get_file_history(vault_path: &str, file_path: &str) -> Result, String> { let vault = Path::new(vault_path); @@ -562,6 +610,57 @@ mod tests { dir } + #[test] + fn test_init_repo_creates_git_directory() { + let dir = TempDir::new().unwrap(); + let vault = dir.path().join("new-vault"); + fs::create_dir_all(&vault).unwrap(); + fs::write(vault.join("note.md"), "# Test\n").unwrap(); + + init_repo(vault.to_str().unwrap()).unwrap(); + + assert!(vault.join(".git").exists()); + } + + #[test] + fn test_init_repo_creates_initial_commit() { + let dir = TempDir::new().unwrap(); + let vault = dir.path().join("new-vault"); + fs::create_dir_all(&vault).unwrap(); + fs::write(vault.join("note.md"), "# Test\n").unwrap(); + + init_repo(vault.to_str().unwrap()).unwrap(); + + let log = Command::new("git") + .args(["log", "--oneline"]) + .current_dir(&vault) + .output() + .unwrap(); + let log_str = String::from_utf8_lossy(&log.stdout); + assert!(log_str.contains("Initial vault setup")); + } + + #[test] + fn test_init_repo_stages_all_files() { + let dir = TempDir::new().unwrap(); + let vault = dir.path().join("new-vault"); + fs::create_dir_all(vault.join("sub")).unwrap(); + fs::write(vault.join("note.md"), "# Test\n").unwrap(); + fs::write(vault.join("sub/nested.md"), "# Nested\n").unwrap(); + + init_repo(vault.to_str().unwrap()).unwrap(); + + let status = Command::new("git") + .args(["status", "--porcelain"]) + .current_dir(&vault) + .output() + .unwrap(); + assert!( + String::from_utf8_lossy(&status.stdout).trim().is_empty(), + "All files should be committed" + ); + } + #[test] fn test_get_file_history_with_commits() { let dir = setup_git_repo(); diff --git a/src-tauri/src/vault/getting_started.rs b/src-tauri/src/vault/getting_started.rs index b2c7a838..aeed2e84 100644 --- a/src-tauri/src/vault/getting_started.rs +++ b/src-tauri/src/vault/getting_started.rs @@ -292,6 +292,8 @@ pub fn create_getting_started_vault(target_path: &str) -> Result .map_err(|e| format!("Failed to write {}: {}", sample.rel_path, e))?; } + crate::git::init_repo(target_path)?; + Ok(vault_dir .canonicalize() .unwrap_or_else(|_| vault_dir.to_path_buf()) @@ -393,4 +395,38 @@ mod tests { let entries = crate::vault::scan_vault(&vault_path).unwrap(); assert_eq!(entries.len(), SAMPLE_FILES.len()); } + + #[test] + fn test_create_getting_started_vault_initializes_git() { + let dir = tempfile::TempDir::new().unwrap(); + let vault_path = dir.path().join("git-vault"); + create_getting_started_vault(vault_path.to_str().unwrap()).unwrap(); + + assert!(vault_path.join(".git").exists()); + + let log = std::process::Command::new("git") + .args(["log", "--oneline"]) + .current_dir(&vault_path) + .output() + .unwrap(); + let log_str = String::from_utf8_lossy(&log.stdout); + assert!(log_str.contains("Initial vault setup")); + } + + #[test] + fn test_create_getting_started_vault_no_untracked_files() { + let dir = tempfile::TempDir::new().unwrap(); + let vault_path = dir.path().join("clean-vault"); + create_getting_started_vault(vault_path.to_str().unwrap()).unwrap(); + + let status = std::process::Command::new("git") + .args(["status", "--porcelain"]) + .current_dir(&vault_path) + .output() + .unwrap(); + assert!( + String::from_utf8_lossy(&status.stdout).trim().is_empty(), + "All files should be committed, no untracked files" + ); + } }