From 17841250762468d8af0d7374962bb30a40b346f3 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Tue, 2 Jun 2026 13:49:56 -0700 Subject: [PATCH] Download commit pack even when commit exists as loose object When TryDownloadCommit finds the commit via CommitAndRootTreeExists, it now checks whether the commit is a loose object. Loose commits (e.g., from a prior 'git show' or 'git log' in a mounted enlistment) do not include reachable trees. Skipping the download in this case causes 'git checkout -f' to fail with 'unable to read tree', followed by an expensive fallback that re-downloads and retries checkout. If the commit is in a pack file (prefetch or commit pack), trees are included by the GVFS protocol, so the download can safely be skipped. Added GitRepo.LooseObjectExists() to check whether a SHA exists as a loose object file in the shared cache or local object store. Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella --- GVFS/GVFS.Common/Git/GitRepo.cs | 24 ++++++++++++++++++++++++ GVFS/GVFS/CommandLine/GVFSVerb.cs | 26 ++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/GVFS/GVFS.Common/Git/GitRepo.cs b/GVFS/GVFS.Common/Git/GitRepo.cs index e5aefa579..933a0fabd 100644 --- a/GVFS/GVFS.Common/Git/GitRepo.cs +++ b/GVFS/GVFS.Common/Git/GitRepo.cs @@ -101,6 +101,30 @@ public virtual bool CommitAndRootTreeExists(string commitSha, out string rootTre return output; } + /// + /// Check whether a given object SHA exists as a loose object file + /// in the shared cache or local object store. + /// + public virtual bool LooseObjectExists(string sha) + { + string looseObjectPath = Path.Combine( + this.enlistment.GitObjectsRoot, + sha.Substring(0, 2), + sha.Substring(2)); + + if (this.fileSystem.FileExists(looseObjectPath)) + { + return true; + } + + looseObjectPath = Path.Combine( + this.enlistment.LocalObjectsRoot, + sha.Substring(0, 2), + sha.Substring(2)); + + return this.fileSystem.FileExists(looseObjectPath); + } + public virtual bool ObjectExists(string blobSha) { bool output = false; diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs index c44608daf..fe3a91e83 100644 --- a/GVFS/GVFS/CommandLine/GVFSVerb.cs +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -487,15 +487,33 @@ protected bool TryDownloadCommit( out string error, bool checkLocalObjectCache = true) { - if (!checkLocalObjectCache || !repo.CommitAndRootTreeExists(commitId, out _)) + if (checkLocalObjectCache && repo.CommitAndRootTreeExists(commitId, out _)) { - if (!gitObjects.TryDownloadCommit(commitId)) + if (repo.LooseObjectExists(commitId)) { - error = "Could not download commit " + commitId + " from: " + Uri.EscapeDataString(objectRequestor.CacheServer.ObjectsEndpointUrl); - return false; + // The commit exists as a loose object (e.g., from a prior 'git show' + // or 'git log' in a mounted enlistment). Loose commits do not include + // their reachable trees — those would need to be fetched individually. + // Download the commit pack which includes all reachable trees so that + // operations like 'git checkout -f' can succeed without the read-object + // hook. + } + else + { + // The commit exists in a pack file (prefetch pack or a previous commit + // pack download). Packs from the GVFS protocol include all reachable + // trees, so we can safely skip re-downloading. + error = null; + return true; } } + if (!gitObjects.TryDownloadCommit(commitId)) + { + error = "Could not download commit " + commitId + " from: " + Uri.EscapeDataString(objectRequestor.CacheServer.ObjectsEndpointUrl); + return false; + } + error = null; return true; }