Skip to content

Commit dcf38a1

Browse files
CopilotViir
andcommitted
Add subdirectory-scoped loading to LoadFromLocalFiles with tests
Agent-Logs-Url: https://github.com/Viir/super-gitcore/sessions/dc5e9d94-d5f6-4842-acce-d0823d169fc2 Co-authored-by: Viir <19209696+Viir@users.noreply.github.com>
1 parent e49f29b commit dcf38a1

5 files changed

Lines changed: 226 additions & 6 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Pure managed C# implementation for reading from Git repositories.
99
+ Reading from local Git repositories
1010
+ Resolve HEAD and other references to commit SHAs.
1111
+ Load all files from any commit's tree.
12+
+ Load files from a specific subdirectory within a commit's tree.
1213
+ Supports both loose objects and pack files.
1314
+ Cloning via [Git Smart HTTP](https://git-scm.com/book/en/v2/Git-on-the-Server-Smart-HTTP)
1415
+ Efficient partial cloning of subdirectories.
@@ -34,6 +35,17 @@ var filesAtHead = GitCore.LoadFromLocalFiles.LoadTreeContentsFromHead(gitDir);
3435
var commitSha = GitCore.LoadFromLocalFiles.ResolveHead(gitDir);
3536
var filesAtCommit = GitCore.LoadFromLocalFiles.LoadTreeContentsFromCommit(gitDir, commitSha);
3637

38+
// Load only files under a specific subdirectory (paths relative to subdirectory)
39+
var subdirFiles = GitCore.LoadFromLocalFiles.LoadSubdirectoryContentsFromHead(
40+
gitDir,
41+
["implement", "Pine.Core"]);
42+
43+
// Or from a specific commit
44+
var subdirFilesAtCommit = GitCore.LoadFromLocalFiles.LoadSubdirectoryContentsFromCommit(
45+
gitDir,
46+
commitSha,
47+
["implement", "Pine.Core"]);
48+
3749
// Resolve any reference (branch, tag, etc.)
3850
var branchSha = GitCore.LoadFromLocalFiles.ResolveReference(gitDir, "refs/heads/main");
3951

implement/GitCore.IntegrationTests/LoadFromLocalFilesTests.cs

Lines changed: 132 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using AwesomeAssertions;
22
using System;
3+
using System.Collections.Generic;
34
using System.Diagnostics;
45
using System.IO;
6+
using System.Linq;
57
using System.Security.Cryptography;
68
using Xunit;
79

@@ -327,6 +329,128 @@ public void Load_tree_contents_from_head()
327329
}
328330
}
329331

332+
[Fact]
333+
public void Load_subdirectory_contents_from_known_commit()
334+
{
335+
var gitDir = _fixture.GitDirectory;
336+
337+
// Use commit 0166a832097feb94bd565354b31559ccb355e0be ("parse commit properties and add API for reading commits") on main
338+
var commitSha = "0166a832097feb94bd565354b31559ccb355e0be";
339+
340+
var subdirContents =
341+
LoadFromLocalFiles.LoadSubdirectoryContentsFromCommit(
342+
gitDir,
343+
commitSha,
344+
["implement", "GitCore"]);
345+
346+
subdirContents.Should().NotBeNull("Subdirectory contents should be loaded");
347+
subdirContents.Count.Should().Be(9, "implement/GitCore should contain 9 files at this commit");
348+
349+
// Verify well-known files exist with paths relative to the subdirectory
350+
subdirContents.Should().ContainKey(["GitCore.csproj"]);
351+
subdirContents.Should().ContainKey(["README.md"]);
352+
subdirContents.Should().ContainKey(["Repository.cs"]);
353+
subdirContents.Should().ContainKey(["GitObjects.cs"]);
354+
subdirContents.Should().ContainKey(["LoadFromUrl.cs"]);
355+
subdirContents.Should().ContainKey(["PackFile.cs"]);
356+
subdirContents.Should().ContainKey(["PackIndex.cs"]);
357+
subdirContents.Should().ContainKey(["GitSmartHttp.cs"]);
358+
subdirContents.Should().ContainKey(["Common", "EnumerableExtensions.cs"]);
359+
360+
// Verify SHA256 hashes of file contents
361+
ComputeSha256Hex(subdirContents[["GitCore.csproj"]]).Should().Be(
362+
"892f9343f5165b461aef51aefd841a78eb1faa73a1ef678d2eadeb0f6dbe2906",
363+
"SHA256 of GitCore.csproj should match");
364+
365+
ComputeSha256Hex(subdirContents[["README.md"]]).Should().Be(
366+
"141f37bb8117bbb98551399270e5b12a0da4767e27bfa63366b72c668b1ccb62",
367+
"SHA256 of README.md should match");
368+
369+
ComputeSha256Hex(subdirContents[["Repository.cs"]]).Should().Be(
370+
"9b183d06142d2fc597d5cb935850fc1770b6be1651156c8665553a13e787bfc0",
371+
"SHA256 of Repository.cs should match");
372+
}
373+
374+
[Fact]
375+
public void Load_subdirectory_contents_from_head()
376+
{
377+
var gitDir = _fixture.GitDirectory;
378+
var repoDir = _fixture.RepoDirectory;
379+
380+
var subdirContents =
381+
LoadFromLocalFiles.LoadSubdirectoryContentsFromHead(
382+
gitDir,
383+
["implement", "GitCore"]);
384+
385+
subdirContents.Should().NotBeNull("Subdirectory contents should be loaded from HEAD");
386+
subdirContents.Count.Should().BeGreaterThan(0, "Subdirectory should contain files");
387+
388+
// Verify well-known files exist with paths relative to the subdirectory
389+
subdirContents.Should().ContainKey(["GitCore.csproj"]);
390+
subdirContents.Should().ContainKey(["README.md"]);
391+
392+
// Verify SHA256 hashes of file contents match git executable output
393+
foreach (var fileName in new[] { "GitCore.csproj", "README.md" })
394+
{
395+
var gitContent = RunGitCommandBytes(repoDir, $"show HEAD:implement/GitCore/{fileName}");
396+
var expectedSha256 = Convert.ToHexStringLower(SHA256.HashData(gitContent));
397+
398+
ComputeSha256Hex(subdirContents[[fileName]]).Should().Be(
399+
expectedSha256,
400+
$"SHA256 of {fileName} from GitCore should match git show output");
401+
}
402+
}
403+
404+
[Fact]
405+
public void Load_subdirectory_contents_matches_full_tree_filtered()
406+
{
407+
var gitDir = _fixture.GitDirectory;
408+
409+
// Use commit 0166a832097feb94bd565354b31559ccb355e0be ("parse commit properties and add API for reading commits") on main
410+
var commitSha = "0166a832097feb94bd565354b31559ccb355e0be";
411+
412+
// Load all files, then filter to the subdirectory
413+
var allFiles = LoadFromLocalFiles.LoadTreeContentsFromCommit(gitDir, commitSha);
414+
415+
var filteredFiles =
416+
new Dictionary<IReadOnlyList<string>, ReadOnlyMemory<byte>>(
417+
comparer: GitCore.Common.EnumerableExtensions.EqualityComparer<IReadOnlyList<string>>());
418+
419+
foreach (var kvp in allFiles)
420+
{
421+
if (kvp.Key.Count >= 3 &&
422+
kvp.Key[0] == "implement" &&
423+
kvp.Key[1] == "GitCore")
424+
{
425+
filteredFiles[(IReadOnlyList<string>)kvp.Key.Skip(2).ToArray()] = kvp.Value;
426+
}
427+
}
428+
429+
// Load subdirectory contents directly
430+
var subdirFiles =
431+
LoadFromLocalFiles.LoadSubdirectoryContentsFromCommit(
432+
gitDir,
433+
commitSha,
434+
["implement", "GitCore"]);
435+
436+
// Both approaches should return the same number of files
437+
subdirFiles.Count.Should().Be(
438+
filteredFiles.Count,
439+
"Subdirectory loading should return the same number of files as filtering the full tree");
440+
441+
// Verify each file matches
442+
foreach (var kvp in filteredFiles)
443+
{
444+
subdirFiles.Should().ContainKey(
445+
kvp.Key,
446+
$"File {string.Join("/", kvp.Key)} should exist in subdirectory contents");
447+
448+
ComputeSha256Hex(subdirFiles[kvp.Key]).Should().Be(
449+
ComputeSha256Hex(kvp.Value),
450+
$"Content of {string.Join("/", kvp.Key)} should match between full tree and subdirectory loading");
451+
}
452+
}
453+
330454
[Fact]
331455
public void FindGitDirectoryUpwards_from_repository_root_finds_git_directory()
332456
{
@@ -352,7 +476,9 @@ public void FindGitDirectoryUpwards_from_subdirectory_finds_git_directory()
352476

353477
result.Should().NotBeNull("Should find .git from a subdirectory");
354478
result.Should().Be(_fixture.GitDirectory, "Should find the correct .git directory");
355-
checkedPaths.Count.Should().BeGreaterThan(1,
479+
480+
checkedPaths.Count.Should().BeGreaterThan(
481+
1,
356482
"Should have checked more than one path when starting below the repo root");
357483
}
358484

@@ -396,9 +522,12 @@ public void FindGitDirectoryUpwards_empty_git_directory_is_skipped()
396522
var result = LoadFromLocalFiles.FindGitDirectoryUpwards(tempDir, out var checkedPaths);
397523

398524
// The empty .git directory should have been checked but not returned
399-
checkedPaths.Should().Contain(Path.Combine(tempDir, ".git"),
525+
checkedPaths.Should().Contain(
526+
Path.Combine(tempDir, ".git"),
400527
"Should have checked the empty .git directory");
401-
result.Should().NotBe(Path.Combine(tempDir, ".git"),
528+
529+
result.Should().NotBe(
530+
Path.Combine(tempDir, ".git"),
402531
"Should not return an empty .git directory");
403532
}
404533
finally

implement/GitCore/GitCore.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
</ItemGroup>
1212

1313
<PropertyGroup>
14-
<AssemblyVersion>0.2.3</AssemblyVersion>
15-
<FileVersion>0.2.3</FileVersion>
14+
<AssemblyVersion>0.2.4</AssemblyVersion>
15+
<FileVersion>0.2.4</FileVersion>
1616
<PackageId>GitCore</PackageId>
17-
<Version>0.2.3</Version>
17+
<Version>0.2.4</Version>
1818
<Description>Pure managed C# implementation for reading from Git repositories</Description>
1919
<PackageTags>git;clone;github;gitlab;network;transport;checkout</PackageTags>
2020
<RepositoryUrl>https://github.com/Viir/GitCore.git</RepositoryUrl>

implement/GitCore/LoadFromLocalFiles.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,73 @@ public static IReadOnlyDictionary<FilePath, ReadOnlyMemory<byte>> LoadTreeConten
187187
return LoadTreeContentsFromCommit(gitDirectory, commitSha);
188188
}
189189

190+
/// <summary>
191+
/// Loads file contents from a subdirectory within the tree of a specific commit
192+
/// in a local repository. Only blobs under the specified subdirectory are materialized.
193+
/// </summary>
194+
/// <param name="gitDirectory">Path to the .git directory.</param>
195+
/// <param name="commitSha">
196+
/// The 40-character hex SHA of the commit.
197+
/// Use <see cref="ResolveReference"/> to obtain this from HEAD or a branch name.
198+
/// </param>
199+
/// <param name="subdirectoryPath">
200+
/// Path components from the repository root to the subdirectory to load.
201+
/// For example, ["implement", "GitCore"] loads only files under implement/GitCore/.
202+
/// </param>
203+
/// <returns>
204+
/// A dictionary mapping file paths (relative to the subdirectory, as lists of path
205+
/// components) to file contents. Only blob entries are included.
206+
/// </returns>
207+
public static IReadOnlyDictionary<FilePath, ReadOnlyMemory<byte>> LoadSubdirectoryContentsFromCommit(
208+
string gitDirectory,
209+
string commitSha,
210+
IReadOnlyList<string> subdirectoryPath)
211+
{
212+
var repository = LoadRepository(gitDirectory);
213+
214+
var commitObject =
215+
repository.GetObject(commitSha)
216+
?? throw new InvalidOperationException($"Commit {commitSha} not found in repository");
217+
218+
if (commitObject.Type is not PackFile.ObjectType.Commit)
219+
{
220+
throw new InvalidOperationException($"Object {commitSha} is not a commit");
221+
}
222+
223+
var commit = GitObjects.ParseCommit(commitObject.Data);
224+
225+
return
226+
GitObjects.GetFilesFromSubdirectory(
227+
commit.TreeHash,
228+
subdirectoryPath,
229+
sha => repository.GetObject(sha));
230+
}
231+
232+
/// <summary>
233+
/// Loads file contents from a subdirectory within the tree at the current HEAD
234+
/// of a local repository. Only blobs under the specified subdirectory are materialized.
235+
/// This is a convenience method that resolves HEAD and then loads the subdirectory.
236+
/// </summary>
237+
/// <param name="gitDirectory">Path to the .git directory.</param>
238+
/// <param name="subdirectoryPath">
239+
/// Path components from the repository root to the subdirectory to load.
240+
/// For example, ["implement", "GitCore"] loads only files under implement/GitCore/.
241+
/// </param>
242+
/// <returns>
243+
/// A dictionary mapping file paths (relative to the subdirectory, as lists of path
244+
/// components) to file contents. Only blob entries are included.
245+
/// </returns>
246+
public static IReadOnlyDictionary<FilePath, ReadOnlyMemory<byte>> LoadSubdirectoryContentsFromHead(
247+
string gitDirectory,
248+
IReadOnlyList<string> subdirectoryPath)
249+
{
250+
var commitSha =
251+
ResolveHead(gitDirectory)
252+
?? throw new InvalidOperationException("Could not resolve HEAD to a commit SHA");
253+
254+
return LoadSubdirectoryContentsFromCommit(gitDirectory, commitSha, subdirectoryPath);
255+
}
256+
190257
/// <summary>
191258
/// Computes the SHA-1 hash of a Git tree object from its entries.
192259
/// This produces the same hash that Git would compute for an equivalent tree.

implement/GitCore/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Pure managed C# implementation for reading from Git repositories.
99
+ Reading from local Git repositories
1010
+ Resolve HEAD and other references to commit SHAs.
1111
+ Load all files from any commit's tree.
12+
+ Load files from a specific subdirectory within a commit's tree.
1213
+ Supports both loose objects and pack files.
1314
+ Cloning via [Git Smart HTTP](https://git-scm.com/book/en/v2/Git-on-the-Server-Smart-HTTP)
1415
+ Efficient partial cloning of subdirectories.
@@ -34,6 +35,17 @@ var filesAtHead = GitCore.LoadFromLocalFiles.LoadTreeContentsFromHead(gitDir);
3435
var commitSha = GitCore.LoadFromLocalFiles.ResolveHead(gitDir);
3536
var filesAtCommit = GitCore.LoadFromLocalFiles.LoadTreeContentsFromCommit(gitDir, commitSha);
3637

38+
// Load only files under a specific subdirectory (paths relative to subdirectory)
39+
var subdirFiles = GitCore.LoadFromLocalFiles.LoadSubdirectoryContentsFromHead(
40+
gitDir,
41+
["implement", "Pine.Core"]);
42+
43+
// Or from a specific commit
44+
var subdirFilesAtCommit = GitCore.LoadFromLocalFiles.LoadSubdirectoryContentsFromCommit(
45+
gitDir,
46+
commitSha,
47+
["implement", "Pine.Core"]);
48+
3749
// Resolve any reference (branch, tag, etc.)
3850
var branchSha = GitCore.LoadFromLocalFiles.ResolveReference(gitDir, "refs/heads/main");
3951

0 commit comments

Comments
 (0)