@@ -289,8 +289,12 @@ where
289289 // Get the current HEAD commit
290290 let head_object_id = self . metadata . head_commit_id ( ) ?;
291291
292- // Load all key-value pairs from the HEAD commit using HistoricalAccess
293- let keys_at_head = self . collect_keys_from_commit_generic ( & head_object_id) ?;
292+ // Load keys via HistoricalAccess::get_keys_at_ref so the Git backend reads
293+ // the per-commit `prolly_hash_mappings` blob (via `collect_keys_at_commit`)
294+ // rather than looking up the commit's root hash in the current in-memory
295+ // mappings, which can be narrower than the commit's view after a
296+ // `git reset` / working-tree switch (see GH-162).
297+ let keys_at_head = self . get_keys_at_ref ( & head_object_id. to_hex ( ) . to_string ( ) ) ?;
294298
295299 // Get the config from the commit
296300 let config = self . read_tree_config_from_commit ( & head_object_id) ?;
@@ -311,18 +315,6 @@ where
311315 Ok ( ( ) )
312316 }
313317
314- /// Collect all key-value pairs from a specific commit (generic version)
315- pub ( super ) fn collect_keys_from_commit_generic (
316- & self ,
317- commit_id : & gix:: ObjectId ,
318- ) -> Result < HashMap < Vec < u8 > , Vec < u8 > > , GitKvError > {
319- // Read the tree config from the commit
320- let tree_config = self . read_tree_config_from_commit ( commit_id) ?;
321-
322- // Use the generic collect_keys_from_config which works for all storage types
323- self . collect_keys_from_config ( & tree_config)
324- }
325-
326318 /// Switch to a different branch or commit (generic version for all backends)
327319 pub fn checkout_generic ( & mut self , branch_or_commit : & str ) -> Result < ( ) , GitKvError >
328320 where
@@ -339,12 +331,77 @@ where
339331 // Update HEAD to point to the new branch
340332 self . metadata . update_head ( branch_or_commit) ?;
341333
334+ // Sync git's index and working tree under the dataset directory to match
335+ // the new HEAD. Without this, prolly's own committed files
336+ // (prolly_config_tree_config, prolly_hash_mappings, ...) retain the previous
337+ // branch's content on disk and `git status` reports them as modified —
338+ // confusing any outside tool working against the repo (see GH-161). Doing
339+ // the sync before `reload_tree_from_head_generic` also makes the in-memory
340+ // tree consistent with what any subsequent git-level reader would see.
341+ self . sync_working_tree_to_head ( ) ?;
342+
342343 // Reload the tree from the HEAD commit
343344 self . reload_tree_from_head_generic ( ) ?;
344345
345346 Ok ( ( ) )
346347 }
347348
349+ /// Restore the working tree and index under the dataset directory to match the
350+ /// current HEAD commit. This is the moral equivalent of `git checkout HEAD -- .`
351+ /// scoped to `dataset_dir`, so non-prolly files living outside the dataset are
352+ /// left untouched. Used by `checkout_generic` (GH-161).
353+ pub ( super ) fn sync_working_tree_to_head ( & self ) -> Result < ( ) , GitKvError > {
354+ let dataset_dir = self
355+ . dataset_dir
356+ . as_ref ( )
357+ . ok_or_else ( || GitKvError :: GitObjectError ( "Dataset directory not set" . into ( ) ) ) ?;
358+ let git_root = self
359+ . metadata
360+ . work_dir ( )
361+ . or_else ( || Self :: find_git_root ( dataset_dir) )
362+ . ok_or_else ( || GitKvError :: GitObjectError ( "Could not find git root" . into ( ) ) ) ?;
363+
364+ let relative = dataset_dir. strip_prefix ( & git_root) . map_err ( |e| {
365+ GitKvError :: GitObjectError ( format ! ( "dataset_dir not under git_root: {e}" ) )
366+ } ) ?;
367+ // `.` when the dataset is the repo root; otherwise the relative path.
368+ let relative_str = if relative. as_os_str ( ) . is_empty ( ) {
369+ "." . to_string ( )
370+ } else {
371+ relative. to_string_lossy ( ) . replace ( '\\' , "/" )
372+ } ;
373+
374+ // `git checkout HEAD -- <path>` rewrites both the index and the working tree
375+ // under <path> to match HEAD. If HEAD doesn't track a file that exists in
376+ // the working tree, git leaves it alone — so user-owned untracked files in
377+ // the dataset directory are preserved. Unlike `git reset --hard`, this does
378+ // not touch anything outside the given pathspec.
379+ let output = std:: process:: Command :: new ( "git" )
380+ . args ( [ "checkout" , "HEAD" , "--" , & relative_str] )
381+ . current_dir ( & git_root)
382+ . output ( )
383+ . map_err ( |e| {
384+ GitKvError :: GitObjectError ( format ! ( "Failed to run `git checkout HEAD -- .`: {e}" ) )
385+ } ) ?;
386+
387+ if !output. status . success ( ) {
388+ let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
389+ // On an empty / non-existent path in HEAD, git complains but we can
390+ // safely treat that as a no-op (e.g. first checkout on a fresh repo).
391+ if stderr. contains ( "did not match any file" )
392+ || stderr. contains ( "pathspec" )
393+ || stderr. contains ( "error: pathspec" )
394+ {
395+ return Ok ( ( ) ) ;
396+ }
397+ return Err ( GitKvError :: GitObjectError ( format ! (
398+ "`git checkout HEAD -- {relative_str}` failed: {stderr}"
399+ ) ) ) ;
400+ }
401+
402+ Ok ( ( ) )
403+ }
404+
348405 /// Merge another branch into the current branch (generic version for all backends)
349406 pub fn merge_generic < R : ConflictResolver > (
350407 & mut self ,
@@ -359,10 +416,19 @@ where
359416 // Find common base commit
360417 let base_commit = self . find_merge_base_generic ( & dest_branch, source_branch) ?;
361418
362- // Get key-value data from each state
363- let base_kv = self . collect_keys_from_commit_generic ( & base_commit) ?;
419+ // Get key-value data from each state via HistoricalAccess::get_keys_at_ref.
420+ //
421+ // The Git backend's specialization reads the per-commit `prolly_hash_mappings`
422+ // blob out of each commit's tree (see `collect_keys_at_commit`), so it works
423+ // even when the working-tree mappings file was narrowed by `git reset` /
424+ // `git checkout` back to a single branch's view (see GH-162). Using the
425+ // generic `collect_keys_from_config` path here would look the commit's root
426+ // hash up in the *current in-memory* mappings and spuriously return an empty
427+ // set for roots that only existed on the other branch.
364428 let source_commit = self . get_branch_commit_generic ( source_branch) ?;
365- let source_kv = self . collect_keys_from_commit_generic ( & source_commit) ?;
429+ let base_kv = self . get_keys_at_ref ( & base_commit. to_hex ( ) . to_string ( ) ) ?;
430+ let source_kv = self . get_keys_at_ref ( & source_commit. to_hex ( ) . to_string ( ) ) ?;
431+
366432 let mut dest_kv = HashMap :: new ( ) ;
367433
368434 for key in self . tree . collect_keys ( ) {
0 commit comments