1- use std:: path:: Path ;
1+ use std:: path:: { Path , PathBuf } ;
2+ use std:: process:: Command ;
23
34use crate :: error:: OxenError ;
45use crate :: lfs:: config:: LfsConfig ;
@@ -54,7 +55,7 @@ pub async fn pull_from_remote(
5455
5556 let store = LocalVersionStore :: new ( & versions_dir) ;
5657 let lfs_config = LfsConfig :: load ( oxen_dir) ?;
57- let mut restored = 0u64 ;
58+ let mut restored_paths : Vec < PathBuf > = Vec :: new ( ) ;
5859
5960 for file_status in & statuses {
6061 if file_status. local {
@@ -63,7 +64,7 @@ pub async fn pull_from_remote(
6364 store
6465 . copy_version_to_path ( & file_status. pointer . oid , & dest)
6566 . await ?;
66- restored += 1 ;
67+ restored_paths . push ( file_status . path . clone ( ) ) ;
6768 } else {
6869 // Try smudge (which checks origin for local clones).
6970 let pointer_data = file_status. pointer . encode ( ) ;
@@ -73,7 +74,7 @@ pub async fn pull_from_remote(
7374 // Smudge resolved it — write to working tree.
7475 let dest = repo_root. join ( & file_status. path ) ;
7576 std:: fs:: write ( & dest, & result) ?;
76- restored += 1 ;
77+ restored_paths . push ( file_status . path . clone ( ) ) ;
7778 } else if !local_only {
7879 // TODO (Phase 3): Fetch from remote, then restore.
7980 log:: warn!(
@@ -84,8 +85,12 @@ pub async fn pull_from_remote(
8485 }
8586 }
8687
87- if restored > 0 {
88- println ! ( "oxen lfs pull: restored {restored} file(s)" ) ;
88+ if !restored_paths. is_empty ( ) {
89+ // Re-add restored files so Git's index stat cache reflects the new
90+ // on-disk content. The clean filter produces the same pointer blob,
91+ // so no actual index change occurs — only the stat cache is updated.
92+ git_add ( repo_root, & restored_paths) ;
93+ println ! ( "oxen lfs pull: restored {} file(s)" , restored_paths. len( ) ) ;
8994 }
9095
9196 Ok ( ( ) )
@@ -113,7 +118,7 @@ pub async fn fetch_all(repo_root: &Path, oxen_dir: &Path) -> Result<(), OxenErro
113118 }
114119
115120 let store = LocalVersionStore :: new ( & versions_dir) ;
116- let mut restored = 0u64 ;
121+ let mut restored_paths : Vec < PathBuf > = Vec :: new ( ) ;
117122 let mut failures: Vec < String > = Vec :: new ( ) ;
118123
119124 for file_status in & statuses {
@@ -124,7 +129,7 @@ pub async fn fetch_all(repo_root: &Path, oxen_dir: &Path) -> Result<(), OxenErro
124129 store
125130 . copy_version_to_path ( & file_status. pointer . oid , & dest)
126131 . await ?;
127- restored += 1 ;
132+ restored_paths . push ( file_status . path . clone ( ) ) ;
128133 println ! ( " restored: {}" , file_status. path. display( ) ) ;
129134 continue ;
130135 }
@@ -142,7 +147,7 @@ pub async fn fetch_all(repo_root: &Path, oxen_dir: &Path) -> Result<(), OxenErro
142147 ) ) ;
143148 } else {
144149 std:: fs:: write ( & dest, & result) ?;
145- restored += 1 ;
150+ restored_paths . push ( file_status . path . clone ( ) ) ;
146151 println ! ( " restored: {}" , file_status. path. display( ) ) ;
147152 }
148153 }
@@ -156,10 +161,42 @@ pub async fn fetch_all(repo_root: &Path, oxen_dir: &Path) -> Result<(), OxenErro
156161 return Err ( OxenError :: basic_str ( msg) ) ;
157162 }
158163
159- println ! ( "oxen lfs fetch-all: all {restored} file(s) restored successfully" ) ;
164+ // Re-add restored files so Git's index stat cache reflects the new
165+ // on-disk content. The clean filter produces the same pointer blob,
166+ // so no actual index change occurs — only the stat cache is updated.
167+ git_add ( repo_root, & restored_paths) ;
168+
169+ println ! (
170+ "oxen lfs fetch-all: all {} file(s) restored successfully" ,
171+ restored_paths. len( )
172+ ) ;
160173 Ok ( ( ) )
161174}
162175
176+ /// Run `git add` on a list of paths so Git's index stat cache is updated.
177+ ///
178+ /// After we replace a pointer file with real content, the on-disk size and
179+ /// mtime change. Without re-adding, `git status` shows the files as modified
180+ /// even though the clean filter produces the identical blob. Re-adding lets
181+ /// Git refresh its stat cache.
182+ fn git_add ( repo_root : & Path , paths : & [ PathBuf ] ) {
183+ if paths. is_empty ( ) {
184+ return ;
185+ }
186+
187+ let path_args: Vec < & str > = paths. iter ( ) . filter_map ( |p| p. to_str ( ) ) . collect ( ) ;
188+ if path_args. is_empty ( ) {
189+ return ;
190+ }
191+
192+ let mut cmd = Command :: new ( "git" ) ;
193+ cmd. arg ( "add" ) . args ( & path_args) . current_dir ( repo_root) ;
194+
195+ if let Err ( e) = cmd. output ( ) {
196+ log:: warn!( "oxen lfs: failed to run git add to refresh index: {e}" ) ;
197+ }
198+ }
199+
163200/// Scan working tree for pointer files and return the list of OIDs
164201/// that need to be pushed.
165202pub async fn list_pushable_oids (
0 commit comments