| File: Pipelines\GitPipelineOutputSnapshotter.cs | Web Access |
| Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Aspire.Hosting.Pipelines; /// <summary> /// Snapshots selected working-tree files using Git's hashes and reported modes. /// </summary> internal static class GitPipelineOutputSnapshotter { /// <summary> /// Captures selected leaves using working-tree modes, reusable index hashes, and residual Git hashing. /// </summary> internal static async Task<IReadOnlyDictionary<string, PipelineOutputFileSnapshot>> CaptureAsync( GitPipelineOutputReader git, IReadOnlyList<GitPipelineOutputReader.SelectedFile> entries, CancellationToken cancellationToken) { var working = await git.ReadStatusAsync(cancellationToken).ConfigureAwait(false); var files = new Dictionary<string, PipelineOutputFileSnapshot>(entries.Count, PipelineOutputRoot.PathComparer); var residual = new List<(FileInfo File, string? Mode)>(); foreach (var entry in entries) { cancellationToken.ThrowIfCancellationRequested(); var path = entry.File.FullName; var state = working.GetValueOrDefault(path); if (state is { IsDeleted: true }) { continue; } // Status supplies the working-tree mode; absent status retains Git's index mode. // Git supplies no mode for an untracked file. Do not infer one from filesystem flags. var mode = state?.Mode ?? entry.Index?.Mode; if (entry.File.IsJunctionOrSymlink()) { var target = entry.File.LinkTarget ?? throw new IOException($"Cannot read pipeline output link target: {path}"); // Both captures hash the same engine-read target text. Index link hashes may use // a different target representation, so they are never reused. var hash = await git.HashLinkTargetAsync(target, cancellationToken).ConfigureAwait(false); files.Add(path, new(IsLink: true, hash, mode)); } else if (entry.Index is { IsReusable: true, IsRegularFile: true } indexed && state?.Y is null or '.') { files.Add(path, new(IsLink: false, indexed.Hash, mode)); } else { residual.Add((entry.File, mode)); } } if (residual.Count != 0) { var hashes = await git.HashFilesAsync(residual.Select(entry => entry.File), cancellationToken).ConfigureAwait(false); if (hashes.Length != residual.Count) { GitPipelineOutputReader.ThrowMalformed("object IDs", "count", [], [new("ExpectedCount", residual.Count), new("ActualCount", hashes.Length)]); } for (var i = 0; i < residual.Count; i++) { files.Add(residual[i].File.FullName, new(IsLink: false, hashes[i], residual[i].Mode)); } } return files; } }