Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@ public async Task Load_tree_from_repository_url_and_commit_sha()
readmeFile.Length.Should().BeGreaterThan(0, "README.md should exist and have content");
}

[Fact]
public async Task Load_subdirectory_tree_from_url_with_commit_sha()
{
// Test loading a subdirectory using a URL with commit SHA
var url = "https://github.com/Viir/GitCore/tree/95e147221ccae4d8609f02f132fc57f87adc135a/implement/GitCore";

// Load the subdirectory contents
var subdirectoryContents = await LoadFromUrl.LoadTreeContentsFromUrlAsync(url);

// Verify that the subdirectory was loaded successfully
subdirectoryContents.Should().NotBeNull("Subdirectory should be loaded");
subdirectoryContents.Count.Should().Be(9, "Subdirectory should contain 9 files");

// Verify specific files exist in the subdirectory
subdirectoryContents.Should().ContainKey(["GitObjects.cs"]);

subdirectoryContents.Should().ContainKey(["LoadFromUrl.cs"]);
}

[Fact]
public async Task Load_tree_with_custom_http_client_for_profiling()
{
Expand Down
111 changes: 111 additions & 0 deletions implement/GitCore.UnitTests/ParseTreeUrlTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using AwesomeAssertions;
using Xunit;

namespace GitCore.UnitTests;

public class ParseTreeUrlTests
{
[Fact]
public void ParseTreeUrl_github_with_branch()
{
var url = "https://github.com/Viir/GitCore/tree/main";
var result = GitSmartHttp.ParseTreeUrl(url);

result.BaseUrl.Should().Be("https://github.com");
result.Owner.Should().Be("Viir");
result.Repo.Should().Be("GitCore");
result.CommitShaOrBranch.Should().Be("main");
result.SubdirectoryPath.Should().BeNull("URL has no subdirectory");
}

[Fact]
public void ParseTreeUrl_github_with_commit_sha()
{
var url = "https://github.com/Viir/GitCore/tree/95e147221ccae4d8609f02f132fc57f87adc135a";
var result = GitSmartHttp.ParseTreeUrl(url);

result.BaseUrl.Should().Be("https://github.com");
result.Owner.Should().Be("Viir");
result.Repo.Should().Be("GitCore");
result.CommitShaOrBranch.Should().Be("95e147221ccae4d8609f02f132fc57f87adc135a");
result.SubdirectoryPath.Should().BeNull("URL has no subdirectory");
}

[Fact]
public void ParseTreeUrl_github_with_branch_and_subdirectory()
{
var url = "https://github.com/Viir/GitCore/tree/main/implement/GitCore";
var result = GitSmartHttp.ParseTreeUrl(url);

result.BaseUrl.Should().Be("https://github.com");
result.Owner.Should().Be("Viir");
result.Repo.Should().Be("GitCore");
result.CommitShaOrBranch.Should().Be("main");
result.SubdirectoryPath.Should().NotBeNull("URL has subdirectory");
result.SubdirectoryPath.Should().HaveCount(2);
result.SubdirectoryPath![0].Should().Be("implement");
result.SubdirectoryPath[1].Should().Be("GitCore");
}

[Fact]
public void ParseTreeUrl_github_with_commit_sha_and_subdirectory()
{
var url = "https://github.com/Viir/GitCore/tree/95e147221ccae4d8609f02f132fc57f87adc135a/implement/GitCore";
var result = GitSmartHttp.ParseTreeUrl(url);

result.BaseUrl.Should().Be("https://github.com");
result.Owner.Should().Be("Viir");
result.Repo.Should().Be("GitCore");
result.CommitShaOrBranch.Should().Be("95e147221ccae4d8609f02f132fc57f87adc135a");
result.SubdirectoryPath.Should().NotBeNull("URL has subdirectory");
result.SubdirectoryPath.Should().HaveCount(2);
result.SubdirectoryPath![0].Should().Be("implement");
result.SubdirectoryPath[1].Should().Be("GitCore");
}

[Fact]
public void ParseTreeUrl_github_with_nested_subdirectory()
{
var url = "https://github.com/Viir/GitCore/tree/main/implement/GitCore/Common";
var result = GitSmartHttp.ParseTreeUrl(url);

result.BaseUrl.Should().Be("https://github.com");
result.Owner.Should().Be("Viir");
result.Repo.Should().Be("GitCore");
result.CommitShaOrBranch.Should().Be("main");
result.SubdirectoryPath.Should().NotBeNull("URL has subdirectory");
result.SubdirectoryPath.Should().HaveCount(3);
result.SubdirectoryPath![0].Should().Be("implement");
result.SubdirectoryPath[1].Should().Be("GitCore");
result.SubdirectoryPath[2].Should().Be("Common");
}

[Fact]
public void ParseTreeUrl_gitlab_with_branch()
{
var url = "https://gitlab.com/owner/repo/-/tree/main";
var result = GitSmartHttp.ParseTreeUrl(url);

result.BaseUrl.Should().Be("https://gitlab.com");
result.Owner.Should().Be("owner");
result.Repo.Should().Be("repo");
result.CommitShaOrBranch.Should().Be("main");
result.SubdirectoryPath.Should().BeNull("URL has no subdirectory");
}

[Fact]
public void ParseTreeUrl_gitlab_with_subdirectory()
{
var url = "https://gitlab.com/owner/repo/-/tree/main/src/test";
var result = GitSmartHttp.ParseTreeUrl(url);

result.BaseUrl.Should().Be("https://gitlab.com");
result.Owner.Should().Be("owner");
result.Repo.Should().Be("repo");
result.CommitShaOrBranch.Should().Be("main");
result.SubdirectoryPath.Should().NotBeNull("URL has subdirectory");
result.SubdirectoryPath.Should().HaveCount(2);
result.SubdirectoryPath![0].Should().Be("src");
result.SubdirectoryPath[1].Should().Be("test");
}
}
33 changes: 20 additions & 13 deletions implement/GitCore/GitSmartHttp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ public record ParseTreeUrlResult(
string BaseUrl,
string Owner,
string Repo,
string CommitShaOrBranch);
string CommitShaOrBranch,
IReadOnlyList<string>? SubdirectoryPath);

/// <summary>
/// Parses a GitHub or GitLab tree URL to extract repository information and commit SHA or branch.
/// </summary>
/// <param name="url">URL like https://github.com/owner/repo/tree/commit-sha or https://github.com/owner/repo/tree/main</param>
/// <returns>Record containing baseUrl, owner, repo, and commitShaOrBranch</returns>
/// <param name="url">URL like https://github.com/owner/repo/tree/commit-sha or https://github.com/owner/repo/tree/main/subdirectory</param>
/// <returns>Record containing baseUrl, owner, repo, commitShaOrBranch, and optional subdirectory path</returns>
public static ParseTreeUrlResult ParseTreeUrl(string url)
{
var uri = new Uri(url);
Expand All @@ -40,22 +41,28 @@ public static ParseTreeUrlResult ParseTreeUrl(string url)

if (host is "github.com" && pathParts.Length >= 4 && pathParts[2] is "tree")
{
// Format: github.com/owner/repo/tree/commit-sha-or-branch
// Format: github.com/owner/repo/tree/commit-sha-or-branch[/subdirectory/path]
var subdirectoryPath = pathParts.Length > 4 ? pathParts[4..] : null;

return new ParseTreeUrlResult(
$"{scheme}://{host}",
pathParts[0],
pathParts[1],
pathParts[3]
BaseUrl: $"{scheme}://{host}",
Owner: pathParts[0],
Repo: pathParts[1],
CommitShaOrBranch: pathParts[3],
SubdirectoryPath: subdirectoryPath
);
}
else if (host is "gitlab.com" && pathParts.Length >= 5 && pathParts[2] is "-" && pathParts[3] is "tree")
{
// Format: gitlab.com/owner/repo/-/tree/commit-sha-or-branch
// Format: gitlab.com/owner/repo/-/tree/commit-sha-or-branch[/subdirectory/path]
var subdirectoryPath = pathParts.Length > 5 ? pathParts[5..] : null;

return new ParseTreeUrlResult(
$"{scheme}://{host}",
pathParts[0],
pathParts[1],
pathParts[4]
BaseUrl: $"{scheme}://{host}",
Owner: pathParts[0],
Repo: pathParts[1],
CommitShaOrBranch: pathParts[4],
SubdirectoryPath: subdirectoryPath
);
}
else
Expand Down
22 changes: 15 additions & 7 deletions implement/GitCore/LoadFromUrl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,14 @@ await GitSmartHttp.FetchBranchCommitShaAsync(
httpClient);
}

// Fetch the pack file containing the commit and its tree
var packFileData =
await GitSmartHttp.FetchPackFileAsync(parsed.BaseUrl, parsed.Owner, parsed.Repo, commitSha, httpClient);
// Load only the subdirectory contents
var gitUrl = $"{parsed.BaseUrl}/{parsed.Owner}/{parsed.Repo}.git";

return LoadTreeContentsFromPackFile(packFileData, commitSha);
return await LoadSubdirectoryContentsFromGitUrlAsync(
gitUrl,
commitSha,
parsed.SubdirectoryPath ?? [],
httpClient);
}

/// <summary>
Expand Down Expand Up @@ -113,8 +116,13 @@ public static async Task<IReadOnlyDictionary<FilePath, ReadOnlyMemory<byte>>> Lo
Func<string, ReadOnlyMemory<byte>?>? getBlobFromCache = null,
Action<string, ReadOnlyMemory<byte>>? reportLoadedBlob = null)
{
return await LoadSubdirectoryContentsWithBloblessCloneAsync(
gitUrl, commitSha, subdirectoryPath, httpClient, getBlobFromCache, reportLoadedBlob);
return await LoadSubdirectoryContentsViaBloblessCloneAsync(
gitUrl: gitUrl,
commitSha: commitSha,
subdirectoryPath: subdirectoryPath,
httpClient,
getBlobFromCache: getBlobFromCache,
reportLoadedBlob: reportLoadedBlob);
}

/// <summary>
Expand Down Expand Up @@ -215,7 +223,7 @@ private static (GitObjects.CommitObject commit, IReadOnlyDictionary<string, Pack
/// Loads subdirectory contents using blobless clone optimization.
/// First fetches only trees and commit, then requests specific blobs for the subdirectory.
/// </summary>
private static async Task<IReadOnlyDictionary<FilePath, ReadOnlyMemory<byte>>> LoadSubdirectoryContentsWithBloblessCloneAsync(
private static async Task<IReadOnlyDictionary<FilePath, ReadOnlyMemory<byte>>> LoadSubdirectoryContentsViaBloblessCloneAsync(
string gitUrl,
string commitSha,
FilePath subdirectoryPath,
Expand Down
Loading