|
8 | 8 | using System.IO; |
9 | 9 | using System.Linq; |
10 | 10 | using System.Runtime.InteropServices; |
| 11 | +using System.Text; |
11 | 12 | using System.Threading; |
12 | 13 | using System.Threading.Tasks; |
13 | 14 |
|
@@ -283,6 +284,84 @@ public void SecondCloneSucceedsWithMissingTrees() |
283 | 284 | File.ReadAllText(Path.Combine(enlistment2.RepoRoot, WellKnownFile)); |
284 | 285 | } |
285 | 286 |
|
| 287 | + [TestCase] |
| 288 | + public void SecondCloneSucceedsWithMissingTreesAndPrefetchPacks() |
| 289 | + { |
| 290 | + // Scenario: prefetch packs exist in shared cache, the target commit and |
| 291 | + // root tree are present as loose objects, but subtrees are missing. |
| 292 | + // The checkout fallback must re-download the commit pack. |
| 293 | + string newCachePath = Path.Combine(this.localCacheParentPath, ".customGvfsCache3"); |
| 294 | + GVFSFunctionalTestEnlistment enlistment1 = this.CreateNewEnlistment(localCacheRoot: newCachePath); |
| 295 | + this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1); |
| 296 | + |
| 297 | + // While mounted, force-download only the commit and root tree for |
| 298 | + // WellKnownBranch. The VFS read-object hook saves these as loose |
| 299 | + // objects in the shared cache. |
| 300 | + string command = "cat-file -p origin/" + WellKnownBranch + "^{tree}"; |
| 301 | + ProcessResult result = GitHelpers.InvokeGitAgainstGVFSRepo(enlistment1.RepoRoot, command); |
| 302 | + result.ExitCode.ShouldEqual(0, $"git {command} failed with error: " + result.Errors); |
| 303 | + |
| 304 | + string packRoot = enlistment1.GetPackRoot(this.fileSystem); |
| 305 | + string objectRoot = enlistment1.GetObjectRoot(this.fileSystem); |
| 306 | + |
| 307 | + // Get a known object SHA to seed the fake prefetch pack (use default |
| 308 | + // branch tip — it's already in the cache as a loose object). |
| 309 | + string seedSha = GitProcess.Invoke(enlistment1.RepoRoot, "rev-parse HEAD").Trim(); |
| 310 | + |
| 311 | + enlistment1.UnmountGVFS(); |
| 312 | + |
| 313 | + // Create the fake prefetch pack BEFORE deleting real packs, so that |
| 314 | + // pack-objects can find the seed object in the existing packs. |
| 315 | + this.CreateMinimalPrefetchPack(enlistment1.RepoRoot, packRoot, seedSha); |
| 316 | + |
| 317 | + // Surgery: delete all NON-fake packs + MIDX so subtrees are gone, but |
| 318 | + // leave loose objects (which include WellKnownBranch commit + root tree). |
| 319 | + this.DeletePackFilesExcept(packRoot, prefix: "prefetch-9999999999"); |
| 320 | + this.DeleteMultiPackIndex(packRoot); |
| 321 | + |
| 322 | + // Clone2 on WellKnownBranch: CommitAndRootTreeExists → true (loose), |
| 323 | + // HasUsablePrefetchPacks → true, but subtrees are missing. |
| 324 | + // The checkout fallback must detect "unable to read tree" and re-download. |
| 325 | + GVFSFunctionalTestEnlistment enlistment2 = this.CreateNewEnlistment(localCacheRoot: newCachePath, branch: WellKnownBranch, skipPrefetch: true); |
| 326 | + File.ReadAllText(Path.Combine(enlistment2.RepoRoot, WellKnownFile)); |
| 327 | + } |
| 328 | + |
| 329 | + [TestCase] |
| 330 | + public void SecondCloneWithPrefetchPacksButMissingCommit() |
| 331 | + { |
| 332 | + // Scenario: prefetch packs exist in the shared cache but the target |
| 333 | + // commit is NOT present. This exercises the deferred-download path |
| 334 | + // (skippedCommitDownload) added by the clone optimization. |
| 335 | + string newCachePath = Path.Combine(this.localCacheParentPath, ".customGvfsCache4"); |
| 336 | + GVFSFunctionalTestEnlistment enlistment1 = this.CreateNewEnlistment(localCacheRoot: newCachePath); |
| 337 | + this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1); |
| 338 | + |
| 339 | + string packRoot = enlistment1.GetPackRoot(this.fileSystem); |
| 340 | + |
| 341 | + // Get a known object SHA to seed the fake prefetch pack. |
| 342 | + string seedSha = GitProcess.Invoke(enlistment1.RepoRoot, "rev-parse HEAD").Trim(); |
| 343 | + |
| 344 | + enlistment1.UnmountGVFS(); |
| 345 | + |
| 346 | + // Create the fake prefetch pack BEFORE deleting real packs, so that |
| 347 | + // pack-objects can find the seed object in the existing packs. |
| 348 | + this.CreateMinimalPrefetchPack(enlistment1.RepoRoot, packRoot, seedSha); |
| 349 | + |
| 350 | + // Surgery: delete all NON-fake packs and MIDX. The real prefetch packs |
| 351 | + // contain WellKnownBranch's commit; removing them ensures it's absent. |
| 352 | + // Leave loose objects and our fake prefetch pack. |
| 353 | + this.DeletePackFilesExcept(packRoot, prefix: "prefetch-9999999999"); |
| 354 | + this.DeleteMultiPackIndex(packRoot); |
| 355 | + |
| 356 | + // Clone2 on WellKnownBranch: CommitAndRootTreeExists → false, |
| 357 | + // HasUsablePrefetchPacks → true, skippedCommitDownload = true. |
| 358 | + // CreateBranchWithUpstream or TryDownloadRootGitAttributes will fail |
| 359 | + // and trigger the deferred commit download. |
| 360 | + GVFSFunctionalTestEnlistment enlistment2 = this.CreateNewEnlistment(localCacheRoot: newCachePath, branch: WellKnownBranch, skipPrefetch: true); |
| 361 | + enlistment2.Status().ShouldContain("Mount status: Ready"); |
| 362 | + File.ReadAllText(Path.Combine(enlistment2.RepoRoot, WellKnownFile)); |
| 363 | + } |
| 364 | + |
286 | 365 | // Override OnTearDownEnlistmentsDeleted rathern than using [TearDown] as the enlistments need to be unmounted before |
287 | 366 | // localCacheParentPath can be deleted (as the SQLite blob sizes database cannot be deleted while GVFS is mounted) |
288 | 367 | protected override void OnTearDownEnlistmentsDeleted() |
@@ -323,5 +402,102 @@ private void HydrateEntireRepo(GVFSFunctionalTestEnlistment enlistment) |
323 | 402 | } |
324 | 403 | } |
325 | 404 | } |
| 405 | + |
| 406 | + /// <summary> |
| 407 | + /// Deletes all .pack, .idx, .keep, .rev, .bitmap, .incomplete files from the |
| 408 | + /// given pack directory EXCEPT those whose file name starts with the given prefix. |
| 409 | + /// </summary> |
| 410 | + private void DeletePackFilesExcept(string packRoot, string prefix) |
| 411 | + { |
| 412 | + foreach (string file in Directory.EnumerateFiles(packRoot)) |
| 413 | + { |
| 414 | + string fileName = Path.GetFileName(file); |
| 415 | + if (fileName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) |
| 416 | + { |
| 417 | + continue; |
| 418 | + } |
| 419 | + |
| 420 | + string ext = Path.GetExtension(file); |
| 421 | + if (ext.Equals(".pack", StringComparison.OrdinalIgnoreCase) |
| 422 | + || ext.Equals(".idx", StringComparison.OrdinalIgnoreCase) |
| 423 | + || ext.Equals(".keep", StringComparison.OrdinalIgnoreCase) |
| 424 | + || ext.Equals(".rev", StringComparison.OrdinalIgnoreCase) |
| 425 | + || ext.Equals(".bitmap", StringComparison.OrdinalIgnoreCase) |
| 426 | + || ext.Equals(".incomplete", StringComparison.OrdinalIgnoreCase)) |
| 427 | + { |
| 428 | + File.SetAttributes(file, FileAttributes.Normal); |
| 429 | + File.Delete(file); |
| 430 | + } |
| 431 | + } |
| 432 | + } |
| 433 | + |
| 434 | + /// <summary> |
| 435 | + /// Deletes the multi-pack-index and related files from the pack directory. |
| 436 | + /// </summary> |
| 437 | + private void DeleteMultiPackIndex(string packRoot) |
| 438 | + { |
| 439 | + foreach (string file in Directory.EnumerateFiles(packRoot)) |
| 440 | + { |
| 441 | + if (Path.GetFileName(file).StartsWith("multi-pack-index", StringComparison.OrdinalIgnoreCase)) |
| 442 | + { |
| 443 | + File.Delete(file); |
| 444 | + } |
| 445 | + } |
| 446 | + } |
| 447 | + |
| 448 | + /// <summary> |
| 449 | + /// Creates a minimal prefetch-named pack containing a single object. |
| 450 | + /// Uses a temporary bare repo to run git pack-objects without VFS hooks. |
| 451 | + /// </summary> |
| 452 | + private void CreateMinimalPrefetchPack(string repoRoot, string packRoot, string objectSha) |
| 453 | + { |
| 454 | + // Create a temporary bare repo that borrows objects from the enlistment |
| 455 | + // via alternates, so pack-objects can find the seed object. |
| 456 | + string tempBareRepo = Path.Combine(Path.GetTempPath(), "gvfs_fakeprefetch_" + Guid.NewGuid().ToString("N").Substring(0, 8)); |
| 457 | + try |
| 458 | + { |
| 459 | + Directory.CreateDirectory(tempBareRepo); |
| 460 | + GitProcess.Invoke(tempBareRepo, "init --bare ."); |
| 461 | + |
| 462 | + // Point alternates at the enlistment's .git/objects and the shared cache |
| 463 | + string alternatesPath = Path.Combine(tempBareRepo, "objects", "info", "alternates"); |
| 464 | + string enlistmentObjects = Path.Combine(repoRoot, ".git", "objects"); |
| 465 | + string alternatesContent = Path.Combine(enlistmentObjects, "info", "alternates"); |
| 466 | + string sharedCacheRoot = File.Exists(alternatesContent) ? File.ReadAllText(alternatesContent).Trim() : ""; |
| 467 | + |
| 468 | + string alternatesLines = enlistmentObjects; |
| 469 | + if (!string.IsNullOrEmpty(sharedCacheRoot)) |
| 470 | + { |
| 471 | + alternatesLines += "\n" + sharedCacheRoot; |
| 472 | + } |
| 473 | + |
| 474 | + File.WriteAllText(alternatesPath, alternatesLines); |
| 475 | + |
| 476 | + // Use pack-objects to create a pack with just the seed object. |
| 477 | + // The prefix includes a fake timestamp so the pack matches the |
| 478 | + // prefetch-<timestamp>-<hash>.pack naming convention. |
| 479 | + string packPrefix = Path.Combine(packRoot, "prefetch-9999999999"); |
| 480 | + |
| 481 | + MemoryStream inputStream = new MemoryStream( |
| 482 | + Encoding.ASCII.GetBytes(objectSha + "\n")); |
| 483 | + |
| 484 | + ProcessResult packResult = GitProcess.InvokeProcess( |
| 485 | + tempBareRepo, |
| 486 | + "-c safe.bareRepository=all pack-objects " + packPrefix, |
| 487 | + inputStream: inputStream); |
| 488 | + packResult.ExitCode.ShouldEqual(0, "git pack-objects failed: " + packResult.Errors); |
| 489 | + |
| 490 | + // pack-objects outputs the pack hash; verify the files were created. |
| 491 | + string packHash = packResult.Output.Trim(); |
| 492 | + string expectedPack = packPrefix + "-" + packHash + ".pack"; |
| 493 | + string expectedIdx = packPrefix + "-" + packHash + ".idx"; |
| 494 | + expectedPack.ShouldBeAFile(this.fileSystem); |
| 495 | + expectedIdx.ShouldBeAFile(this.fileSystem); |
| 496 | + } |
| 497 | + finally |
| 498 | + { |
| 499 | + RepositoryHelpers.DeleteTestDirectory(tempBareRepo); |
| 500 | + } |
| 501 | + } |
326 | 502 | } |
327 | 503 | } |
0 commit comments