From 096f62907c7d39a4dc244d227531bcc8197408b0 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Fri, 18 Sep 2026 20:20:46 +0200 Subject: [PATCH] feat(file-history): mark latest history entry identical to working tree Annotate file history entries with matches_working_tree and surface that information in the UI so the most recent commit can be identified as "current version" when its content equals the working tree. - Backend: add FileHistoryCommit and annotate_file_history(...) which checks (only for regular files) whether the newest commit's blob matches the working tree via `git diff --quiet`. list_file_history now returns the annotated commits. - Types: add optional matches_working_tree to GitCommit shape used by the UI. - UI: show a "Current version"/"Aktueller Stand" badge and disable Diff/Restore actions for entries that match the working tree (text localized for de/en). Also add a test that verifies the matching behavior across file edits, staging, committing and deletion. No external API breaking changes. --- src-tauri/src/git.rs | 47 ++++++++++++++++++++- src/App.svelte | 1 + src/lib/components/FileHistoryDialog.svelte | 11 ++++- src/lib/types.ts | 2 + 4 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index b3962ae..6354bba 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -4334,6 +4334,29 @@ pub async fn list_repository_files(path: String) -> Result, cancellation: Option<&SearchCancellation>) -> Result, String> { + // Keep folder history unchanged: Git diff would omit untracked children. + let regular_file = fs::symlink_metadata(repo.join(file)).is_ok_and(|metadata| metadata.is_file()); + let mut result = Vec::with_capacity(commits.len()); + for (index, commit) in commits.into_iter().enumerate() { + check_search_cancelled(cancellation)?; + let matches_working_tree = index == 0 && regular_file && run_git_cancellable( + repo, ["diff", "--quiet", "--no-ext-diff", "--no-textconv", &commit.hash, "--", file], + cancellation, "Could not compare current file version", + ).is_ok(); + check_search_cancelled(cancellation)?; + result.push(FileHistoryCommit { commit, matches_working_tree }); + } + Ok(result) +} + #[tauri::command] pub async fn list_file_history( path: String, @@ -4341,7 +4364,7 @@ pub async fn list_file_history( limit: Option, request_id: Option, state: tauri::State<'_, SearchCancellationState>, -) -> Result, String> { +) -> Result, String> { let state = state.inner().clone(); tauri::async_runtime::spawn_blocking(move || { @@ -4354,7 +4377,8 @@ pub async fn list_file_history( search_id: request_id.clone(), }); - let result = list_file_history_core(&repo, file, limit, cancellation.as_ref()); + let result = list_file_history_core(&repo, file.clone(), limit, cancellation.as_ref()) + .and_then(|commits| annotate_file_history(&repo, &file, commits, cancellation.as_ref())); if let Some(request_id) = request_id.as_deref() { let _ = state.clear(request_id); @@ -10275,6 +10299,25 @@ mod tests { assert_eq!(commits[1].summary, "init"); } + #[test] + fn file_history_marks_only_an_identical_current_file() { + let repo = init_temp_repo("history_current_version"); + commit_initial_file(&repo.path); + let matches = || { + let commits = list_file_history_core(&repo.path, "old.txt".into(), Some(10), None).unwrap(); + annotate_file_history(&repo.path, "old.txt", commits, None).unwrap()[0].matches_working_tree + }; + assert!(matches()); + fs::write(repo.path.join("old.txt"), "local changes\n").unwrap(); + assert!(!matches()); + run_git_test(&repo.path, ["add", "old.txt"]); + assert!(!matches()); + run_git_test(&repo.path, ["commit", "-q", "-m", "updated"]); + assert!(matches()); + fs::remove_file(repo.path.join("old.txt")).unwrap(); + assert!(!matches()); + } + #[test] fn list_file_history_returns_commits_for_selected_folder() { let repo = init_temp_repo("folder_history"); diff --git a/src/App.svelte b/src/App.svelte index 394857e..d6e9af9 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -6730,6 +6730,7 @@ void; onRestore: (commit: GitCommit) => void; onClose: () => void; @@ -24,6 +25,7 @@ isBusy = false, isLoading = false, error = "", + language = "en", onDiff = () => {}, onRestore = () => {}, onClose = () => {}, @@ -86,14 +88,15 @@
{item.summary} {item.short_hash} ยท {item.author_name} + {#if item.matches_working_tree}{language === "de" ? "Aktueller Stand" : "Current version"}{/if}
- -
@@ -104,3 +107,7 @@ + + diff --git a/src/lib/types.ts b/src/lib/types.ts index f3e69dd..9670142 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -260,6 +260,8 @@ export interface GitStash { } export interface GitCommit { + /** Set for the latest file-history entry when its diff against the working tree is empty. */ + matches_working_tree?: boolean; hash: string; short_hash: string; summary: string;