From e1cfaa93bf78cb79f46519a80488cee743a8dfd2 Mon Sep 17 00:00:00 2001 From: Surma Date: Sat, 7 Nov 2026 02:39:08 +0000 Subject: [PATCH] git: refresh index stat cache after CLI status --- userland/git/README.md | 5 +++++ userland/git/src/changes.rs | 20 +++++++++++++++++--- userland/git/src/lib.rs | 2 +- userland/git/test.nu | 24 ++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/userland/git/README.md b/userland/git/README.md index 3748d83..13e2f39 100644 --- a/userland/git/README.md +++ b/userland/git/README.md @@ -92,6 +92,11 @@ The trailer follows a blank line after the message body. Without this variable, `commit` preserves the normal message. The tests compare both forms with native Git, including commit IDs. `clean -fdq` preserves tracked files, ignored files, and nested repositories. +CLI `status` refreshes cached file stats in the index after the content comparison. +Repeated status calls reuse those stats instead of hashing unchanged files again. +If the index cannot accept writes, status still reports changes. +The read-only status API does not refresh the index. +The comparison suite checks cache refresh, cache reuse, and same-size content edits. The log applies its path filter before its count limit. Stashes use ordinary Git commits and the `refs/stash` reflog. diff --git a/userland/git/src/changes.rs b/userland/git/src/changes.rs index a6447b7..6b2f91f 100644 --- a/userland/git/src/changes.rs +++ b/userland/git/src/changes.rs @@ -1,4 +1,4 @@ -//! Read-only snapshots and patches. Ordinary Git state only; never refresh/write the index. +//! Snapshots and patches. Only CLI status refreshes the ordinary Git index stat cache. use crate::Result; use crate::{ HeadInfo, head_info, @@ -223,6 +223,13 @@ fn changes(old: &Snapshot, new: &Snapshot) -> Vec { .collect() } pub fn status(repo: &gix::Repository) -> Result { + status_inner(repo, false) +} +/// CLI status refreshes cached file stats. RPC status remains read-only. +pub fn refresh_status(repo: &gix::Repository) -> Result { + status_inner(repo, true) +} +fn status_inner(repo: &gix::Repository, refresh: bool) -> Result { repo::worktree(repo)?; let (index, conflicts) = index(repo)?; let mut head = tree(repo, "HEAD")?; @@ -237,13 +244,13 @@ pub fn status(repo: &gix::Repository) -> Result { conflicted: conflicts.iter().map(|p| display(p)).collect(), }; use gix::status::index_worktree::{Item, iter::Summary}; - let iter = repo + let mut iter = repo .status(gix::progress::Discard) .map_err(|e| error("status", e))? .untracked_files(gix::status::UntrackedFiles::Files) .into_index_worktree_iter(Vec::::new()) .map_err(|e| error("status", e))?; - for item in iter { + for item in iter.by_ref() { let item = item.map_err(|e| error("status", e))?; #[cfg(target_os = "wasi")] if matches!( @@ -285,6 +292,13 @@ pub fn status(repo: &gix::Repository) -> Result { }, }); } + if refresh + && let Some(mut outcome) = iter.into_outcome() + && outcome.has_changes() + { + // A read-only repository must still support status, like native Git. + let _ = outcome.write_changes(); + } result.unstaged.sort_by(|a, b| a.path.cmp(&b.path)); result.untracked.sort(); Ok(result) diff --git a/userland/git/src/lib.rs b/userland/git/src/lib.rs index 169ce75..f4bcf27 100644 --- a/userland/git/src/lib.rs +++ b/userland/git/src/lib.rs @@ -808,7 +808,7 @@ pub fn run(cli: Cli) -> Result { porcelain, branch, } => { - let mut status = changes::status(&repository)?; + let mut status = changes::refresh_status(&repository)?; let index = repository .index_or_empty() .map_err(|e| error("status index", e))?; diff --git a/userland/git/test.nu b/userland/git/test.nu index 3d823e3..97ffa39 100644 --- a/userland/git/test.nu +++ b/userland/git/test.nu @@ -384,6 +384,30 @@ def main [component: path, server: path = '/tmp/p1-git/server-target/debug/git-t $executable_ok = ($executable_ok and $indexed.exit_code == 0 and ($indexed.stdout | str starts-with '100755 ')) print $'executable checkout stays clean: (if $executable_ok {"PASS"} else {"FAIL"})' if not $executable_ok { $failures += 1 } + # Status refreshes stale stats once, then detects a same-size content change. + let cached = ($root | path join cached) + mkdir $cached + ^git -C $cached init -q -b main + 'base' | save ($cached | path join file.txt) + ^git -C $cached add file.txt + ^git -C $cached commit --no-gpg-sign -q -m cached + ^touch -m -d @1700000000 ($cached | path join file.txt) + let index = ($cached | path join .git/index) + let before = (open --raw $index | hash sha256) + mut cached_ok = true + mut refreshed = '' + for pass in [1 2] { + let result = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($cached)::/cached' $component -C /cached status --short | complete) + let current = (open --raw $index | hash sha256) + $cached_ok = ($cached_ok and $result.exit_code == 0 and $result.stdout == '') + if $pass == 1 { $cached_ok = ($cached_ok and $current != $before); $refreshed = $current } else { $cached_ok = ($cached_ok and $current == $refreshed) } + } + 'edit' | save --force ($cached | path join file.txt) + let changed = (^wasmtime run -S http=y -S cli-exit-with-code=y --dir $'($cached)::/cached' $component -C /cached status --short | complete) + let native_changed = (^git -C $cached status --short | complete) + $cached_ok = ($cached_ok and $changed.exit_code == 0 and $changed.stdout == $native_changed.stdout and $changed.stdout == " M file.txt\n") + print $'status caches stats and detects edits: (if $cached_ok {"PASS"} else {"FAIL"})' + if not $cached_ok { $failures += 1 } # A merge tree that differs from both parents requires a combined diff. 'combined resolution\n' | save --force ($root | path join native/repo/file.txt) let native = ($root | path join native/repo)