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.
This commit is contained in:
+45
-2
@@ -4334,6 +4334,29 @@ pub async fn list_repository_files(path: String) -> Result<Vec<GitRepositoryFile
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FileHistoryCommit {
|
||||
#[serde(flatten)]
|
||||
commit: GitCommit,
|
||||
matches_working_tree: bool,
|
||||
}
|
||||
|
||||
fn annotate_file_history(repo: &Path, file: &str, commits: Vec<GitCommit>, cancellation: Option<&SearchCancellation>) -> Result<Vec<FileHistoryCommit>, 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<u32>,
|
||||
request_id: Option<String>,
|
||||
state: tauri::State<'_, SearchCancellationState>,
|
||||
) -> Result<Vec<GitCommit>, String> {
|
||||
) -> Result<Vec<FileHistoryCommit>, 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");
|
||||
|
||||
@@ -6730,6 +6730,7 @@
|
||||
<module.default
|
||||
{fileHistory}
|
||||
filePath={selectedExplorerPath}
|
||||
language={appLanguage}
|
||||
{isBusy}
|
||||
isLoading={fileHistoryLoading}
|
||||
error={fileHistoryError}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
isBusy: boolean;
|
||||
isLoading: boolean;
|
||||
error?: string;
|
||||
language?: "de" | "en";
|
||||
onDiff: (commit: GitCommit) => 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 @@
|
||||
<div>
|
||||
<strong title={item.summary}>{item.summary}</strong>
|
||||
<span><code>{item.short_hash}</code> · {item.author_name}</span>
|
||||
{#if item.matches_working_tree}<span class="current-version">{language === "de" ? "Aktueller Stand" : "Current version"}</span>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<time datetime={item.date}>{formatCommitDate(item.date)}</time>
|
||||
<div class="file-history-dialog-actions">
|
||||
<button class="btn-sm" type="button" onclick={() => onDiff(item)} disabled={isBusy} title="Show changes against the working tree">
|
||||
<button class="btn-sm" type="button" onclick={() => onDiff(item)} disabled={isBusy || item.matches_working_tree} title={item.matches_working_tree ? (language === "de" ? "Identisch mit der aktuellen Datei" : "Identical to the current file") : "Show changes against the working tree"}>
|
||||
<GitCompare size={14} aria-hidden="true" /> Diff
|
||||
</button>
|
||||
<button class="btn-sm" type="button" onclick={() => onRestore(item)} disabled={isBusy} title="Restore this file from the selected commit">
|
||||
<button class="btn-sm" type="button" onclick={() => onRestore(item)} disabled={isBusy || item.matches_working_tree} title={item.matches_working_tree ? (language === "de" ? "Identisch mit der aktuellen Datei" : "Identical to the current file") : "Restore this file from the selected commit"}>
|
||||
<RotateCcw size={14} aria-hidden="true" /> Restore
|
||||
</button>
|
||||
</div>
|
||||
@@ -104,3 +107,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.file-history-dialog-commit .current-version{display:inline-flex;width:fit-content;margin-top:4px;padding:2px 6px;border:1px solid color-mix(in srgb,var(--color-accent) 25%,transparent);border-radius:4px;background:color-mix(in srgb,var(--color-accent) 8%,transparent);color:var(--color-accent);font-size:10px;font-weight:600}
|
||||
</style>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user