feat: ensure .DS_Store in .gitignore for all new vaults

Extract ensure_gitignore from init_repo so it can be reused by
clone_repo (GitHub "Create New" flow) and repair_vault. New vaults
created via any path now get .DS_Store excluded by default.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test
2026-03-16 05:36:45 +01:00
parent f89b199b79
commit fa74009877
4 changed files with 66 additions and 17 deletions

View File

@@ -556,6 +556,8 @@ pub fn repair_vault(vault_path: String) -> Result<String, String> {
theme::restore_default_themes(&vault_path)?;
// Repair config files (config/agents.md, type/config.md, AGENTS.md stub)
vault::repair_config_files(&vault_path)?;
// Ensure .gitignore with sensible defaults exists
git::ensure_gitignore(&vault_path)?;
Ok("Vault repaired".to_string())
}

View File

@@ -30,20 +30,7 @@ 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)?;
// Write .gitignore before the first commit so machine-specific and
// macOS metadata files are never tracked and don't cause conflicts.
let gitignore_path = dir.join(".gitignore");
if !gitignore_path.exists() {
std::fs::write(
&gitignore_path,
"# Laputa app files (machine-specific, never commit)\n\
const DEFAULT_GITIGNORE: &str = "# Laputa app files (machine-specific, never commit)\n\
.laputa/settings.json\n\
\n\
# macOS\n\
@@ -58,10 +45,29 @@ pub fn init_repo(path: &str) -> Result<(), String> {
.vscode/\n\
.idea/\n\
*.swp\n\
*.swo\n",
)
.map_err(|e| format!("Failed to write .gitignore: {}", e))?;
*.swo\n";
/// Ensure a `.gitignore` with sensible defaults exists in the vault directory.
/// Creates the file if missing; leaves existing `.gitignore` files untouched.
pub fn ensure_gitignore(path: &str) -> Result<(), String> {
let gitignore_path = Path::new(path).join(".gitignore");
if !gitignore_path.exists() {
std::fs::write(&gitignore_path, DEFAULT_GITIGNORE)
.map_err(|e| format!("Failed to write .gitignore: {}", e))?;
}
Ok(())
}
/// 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)?;
// Write .gitignore before the first commit so machine-specific and
// macOS metadata files are never tracked and don't cause conflicts.
ensure_gitignore(path)?;
run_git(dir, &["add", "."])?;
run_git(dir, &["commit", "-m", "Initial vault setup"])?;
@@ -212,6 +218,29 @@ mod tests {
(bare_dir, clone_a_dir, clone_b_dir)
}
#[test]
fn test_ensure_gitignore_creates_file() {
let dir = TempDir::new().unwrap();
let path = dir.path().to_str().unwrap();
ensure_gitignore(path).unwrap();
let content = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
assert!(content.contains(".DS_Store"));
assert!(content.contains(".laputa/settings.json"));
}
#[test]
fn test_ensure_gitignore_preserves_existing() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join(".gitignore"), "my-rule\n").unwrap();
ensure_gitignore(dir.path().to_str().unwrap()).unwrap();
let content = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
assert_eq!(content, "my-rule\n");
}
#[test]
fn test_init_repo_creates_git_directory() {
let dir = TempDir::new().unwrap();

View File

@@ -37,6 +37,9 @@ pub fn clone_repo(url: &str, token: &str, local_path: &str) -> Result<String, St
// Configure the remote to use token auth for future pushes
configure_remote_auth(local_path, url, token)?;
// Ensure sensible .gitignore defaults (especially .DS_Store on macOS)
crate::git::ensure_gitignore(local_path)?;
Ok(format!("Cloned to {}", local_path))
}

View File

@@ -0,0 +1,15 @@
import { test, expect } from '@playwright/test'
import { openCommandPalette, findCommand } from './helpers'
test.describe('DS_Store gitignore for new vaults', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.waitForLoadState('networkidle')
})
test('Repair Vault command is available in the command palette', async ({ page }) => {
await openCommandPalette(page)
const found = await findCommand(page, 'Repair Vault')
expect(found).toBe(true)
})
})