77import static org .mockito .Mockito .when ;
88
99import java .io .*;
10+ import java .nio .file .Files ;
11+ import java .nio .file .NoSuchFileException ;
1012import java .util .*;
1113import org .eclipse .jgit .api .Git ;
1214import org .eclipse .jgit .api .LogCommand ;
@@ -190,8 +192,6 @@ void testFileLogReturnsOnlyVerifiedPartialPathHistoryWhenFilteredWalkYieldsNothi
190192 void testFileLogReturnsOnlyVerifiedPartialPathHistoryWithMissingTree () throws Exception {
191193 // Simulates a shallow clone whose tree objects are missing: the filtered walk
192194 // may yield zero results due to missing objects, but should not fall back to total history.
193- GitLogReader gitLogReader = new GitLogReader (git );
194-
195195 String attributeHandler = "AttributeHandler.java" ;
196196 InputStream resourceAsStream = getClass ().getClassLoader ().getResourceAsStream (attributeHandler );
197197 String contents = convertInputStreamToString (resourceAsStream );
@@ -208,11 +208,19 @@ void testFileLogReturnsOnlyVerifiedPartialPathHistoryWithMissingTree() throws Ex
208208
209209 deleteLooseObject (secondCommit .getTree ());
210210
211- ScmLogInfo scmLogInfo = Assertions .assertDoesNotThrow (() -> gitLogReader .fileLog (attributeHandler ));
212-
213- // With missing tree objects, the filtered walk may yield zero verified partial results
214- // rather than falling back to total repository history
215- Assertions .assertEquals (0 , scmLogInfo .getCommitCount ());
211+ // Read through a freshly opened repository: the commits above populated JGit's
212+ // in-memory loose-object cache (UnpackedObjectCache, one per ObjectDirectory
213+ // instance), which could otherwise serve the deleted tree from memory and let
214+ // the walk succeed with 2 commits instead of failing. A new Repository instance
215+ // gets an empty cache, so the missing object is really exercised on every OS.
216+ try (Git freshGit = Git .open (repository .getDirectory ())) {
217+ GitLogReader gitLogReader = new GitLogReader (freshGit );
218+ ScmLogInfo scmLogInfo = Assertions .assertDoesNotThrow (() -> gitLogReader .fileLog (attributeHandler ));
219+
220+ // With missing tree objects, the filtered walk may yield zero verified partial results
221+ // rather than falling back to total repository history
222+ Assertions .assertEquals (0 , scmLogInfo .getCommitCount ());
223+ }
216224 }
217225
218226 @ Test
@@ -232,16 +240,39 @@ void testFileLogDoesNotFabricateHistoryForFileNeverCommitted() throws Exception
232240 Assertions .assertEquals (0 , scmLogInfo .getCommitCount ());
233241 }
234242
235- private void deleteLooseObject (ObjectId objectId ) throws IOException {
243+ private void deleteLooseObject (ObjectId objectId ) throws IOException , InterruptedException {
236244 String objectName = objectId .getName ();
237245 File looseObject = new File (
238246 new File (repository .getDirectory (), "objects" ),
239247 objectName .substring (0 , 2 ) + '/' + objectName .substring (2 ));
240248 org .junit .jupiter .api .Assumptions .assumeTrue (
241249 looseObject .exists (), "loose object " + objectName + " should exist in a fresh repository" );
242- if (!looseObject .delete ()) {
243- throw new IOException ("Unable to delete loose object " + looseObject );
250+
251+ // JGit writes loose objects with the read-only attribute set (as C git
252+ // does). POSIX unlink ignores the read-only bit, but Windows refuses to
253+ // delete a read-only file and every retry would fail with
254+ // AccessDeniedException — so the attribute must be cleared first.
255+ if (!looseObject .setWritable (true ) && !looseObject .canWrite ()) {
256+ throw new IOException ("Unable to clear read-only flag on loose object " + looseObject );
257+ }
258+
259+ // On Windows, antivirus or the search indexer can also hold a newly
260+ // created file open briefly, making an immediate delete fail
261+ // spuriously. Retry for a short window before giving up (harmless on
262+ // Linux/macOS, where the first attempt succeeds).
263+ IOException lastError = null ;
264+ for (int attempt = 0 ; attempt < 50 ; attempt ++) {
265+ try {
266+ Files .delete (looseObject .toPath ());
267+ return ;
268+ } catch (NoSuchFileException alreadyGone ) {
269+ return ;
270+ } catch (IOException lockedOrBusy ) {
271+ lastError = lockedOrBusy ;
272+ Thread .sleep (50 );
273+ }
244274 }
275+ throw new IOException ("Unable to delete loose object " + looseObject , lastError );
245276 }
246277
247278 @ Test
0 commit comments