feat: detect external file renames and offer wikilink update via banner
When the app regains focus, checks git diff for renames (--diff-filter=R). If renamed .md files are found, shows a non-blocking banner with "Update wikilinks" and "Ignore" buttons. The update reuses the existing vault-wide wikilink replacement logic from rename.rs. New Tauri commands: detect_renames, update_wikilinks_for_renames. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use crate::frontmatter::FrontmatterValue;
|
||||
use crate::search::SearchResponse;
|
||||
use crate::vault::{FolderNode, RenameResult, VaultEntry};
|
||||
use crate::vault::{DetectedRename, FolderNode, RenameResult, VaultEntry};
|
||||
use crate::{frontmatter, git, search, vault};
|
||||
|
||||
use super::expand_tilde;
|
||||
@@ -43,6 +43,18 @@ pub fn rename_note(
|
||||
vault::rename_note(&vault_path, &old_path, &new_title, old_title.as_deref())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn detect_renames(vault_path: String) -> Result<Vec<DetectedRename>, String> {
|
||||
let vault_path = expand_tilde(&vault_path);
|
||||
vault::detect_renames(&vault_path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_wikilinks_for_renames(vault_path: String, renames: Vec<DetectedRename>) -> Result<usize, String> {
|
||||
let vault_path = expand_tilde(&vault_path);
|
||||
vault::update_wikilinks_for_renames(&vault_path, &renames)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn purge_trash(vault_path: String) -> Result<Vec<String>, String> {
|
||||
let vault_path = expand_tilde(&vault_path);
|
||||
|
||||
@@ -123,6 +123,8 @@ pub fn run() {
|
||||
commands::update_frontmatter,
|
||||
commands::delete_frontmatter_property,
|
||||
commands::rename_note,
|
||||
commands::detect_renames,
|
||||
commands::update_wikilinks_for_renames,
|
||||
commands::get_file_history,
|
||||
commands::get_modified_files,
|
||||
commands::get_file_diff,
|
||||
|
||||
@@ -18,7 +18,7 @@ pub use file::{get_note_content, save_note_content};
|
||||
pub use getting_started::{create_getting_started_vault, default_vault_path, vault_exists};
|
||||
pub use image::{copy_image_to_vault, save_image};
|
||||
pub use migration::{flatten_vault, migrate_is_a_to_type, vault_health_check, VaultHealthReport};
|
||||
pub use rename::{rename_note, RenameResult};
|
||||
pub use rename::{detect_renames, rename_note, update_wikilinks_for_renames, DetectedRename, RenameResult};
|
||||
pub use title_sync::{sync_title_on_open, SyncAction};
|
||||
pub use trash::{batch_delete_notes, delete_note, empty_trash, is_file_trashed, purge_trash};
|
||||
|
||||
|
||||
@@ -250,6 +250,77 @@ pub fn rename_note(
|
||||
})
|
||||
}
|
||||
|
||||
/// A detected rename: old path → new path (both relative to vault root).
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DetectedRename {
|
||||
pub old_path: String,
|
||||
pub new_path: String,
|
||||
}
|
||||
|
||||
/// Detect renamed files by comparing working tree against HEAD using git diff.
|
||||
pub fn detect_renames(vault_path: &str) -> Result<Vec<DetectedRename>, String> {
|
||||
let vault = Path::new(vault_path);
|
||||
let output = std::process::Command::new("git")
|
||||
.args(["diff", "HEAD", "--name-status", "--diff-filter=R", "-M"])
|
||||
.current_dir(vault)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run git diff: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(vec![]); // No HEAD yet or other git issue — no renames
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let renames: Vec<DetectedRename> = stdout
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let parts: Vec<&str> = line.split('\t').collect();
|
||||
if parts.len() >= 3 && parts[0].starts_with('R') {
|
||||
let old = parts[1].to_string();
|
||||
let new = parts[2].to_string();
|
||||
if old.ends_with(".md") && new.ends_with(".md") {
|
||||
return Some(DetectedRename { old_path: old, new_path: new });
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(renames)
|
||||
}
|
||||
|
||||
/// Update wikilinks across the vault for a list of detected renames.
|
||||
/// Returns the total number of files updated.
|
||||
pub fn update_wikilinks_for_renames(vault_path: &str, renames: &[DetectedRename]) -> Result<usize, String> {
|
||||
let vault = Path::new(vault_path);
|
||||
let mut total_updated = 0;
|
||||
|
||||
for rename in renames {
|
||||
let old_stem = rename.old_path.strip_suffix(".md").unwrap_or(&rename.old_path);
|
||||
let new_stem = rename.new_path.strip_suffix(".md").unwrap_or(&rename.new_path);
|
||||
let old_filename_stem = old_stem.split('/').last().unwrap_or(old_stem);
|
||||
let new_filename_stem = new_stem.split('/').last().unwrap_or(new_stem);
|
||||
|
||||
// Build title from filename stem (kebab-case → Title Case)
|
||||
let old_title = super::parsing::slug_to_title(old_filename_stem);
|
||||
let new_title = super::parsing::slug_to_title(new_filename_stem);
|
||||
|
||||
// The new file is the exclude target (don't rewrite wikilinks inside the renamed file itself)
|
||||
let new_file = vault.join(&rename.new_path);
|
||||
|
||||
let updated = update_wikilinks_in_vault(&WikilinkReplacement {
|
||||
vault_path: vault,
|
||||
old_title: &old_title,
|
||||
new_title: &new_title,
|
||||
old_path_stem: old_filename_stem,
|
||||
exclude_path: &new_file,
|
||||
});
|
||||
total_updated += updated;
|
||||
}
|
||||
|
||||
Ok(total_updated)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user