diff --git a/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs b/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs index 47e6531..af9aa2e 100644 --- a/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs +++ b/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs @@ -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() { diff --git a/implement/GitCore.UnitTests/ParseTreeUrlTests.cs b/implement/GitCore.UnitTests/ParseTreeUrlTests.cs new file mode 100644 index 0000000..a691be4 --- /dev/null +++ b/implement/GitCore.UnitTests/ParseTreeUrlTests.cs @@ -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"); + } +} diff --git a/implement/GitCore/GitSmartHttp.cs b/implement/GitCore/GitSmartHttp.cs index f2c28a7..d5f335e 100644 --- a/implement/GitCore/GitSmartHttp.cs +++ b/implement/GitCore/GitSmartHttp.cs @@ -24,13 +24,14 @@ public record ParseTreeUrlResult( string BaseUrl, string Owner, string Repo, - string CommitShaOrBranch); + string CommitShaOrBranch, + IReadOnlyList? SubdirectoryPath); /// /// Parses a GitHub or GitLab tree URL to extract repository information and commit SHA or branch. /// - /// URL like https://github.com/owner/repo/tree/commit-sha or https://github.com/owner/repo/tree/main - /// Record containing baseUrl, owner, repo, and commitShaOrBranch + /// URL like https://github.com/owner/repo/tree/commit-sha or https://github.com/owner/repo/tree/main/subdirectory + /// Record containing baseUrl, owner, repo, commitShaOrBranch, and optional subdirectory path public static ParseTreeUrlResult ParseTreeUrl(string url) { var uri = new Uri(url); @@ -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 diff --git a/implement/GitCore/LoadFromUrl.cs b/implement/GitCore/LoadFromUrl.cs index 62c6793..ef48b4e 100644 --- a/implement/GitCore/LoadFromUrl.cs +++ b/implement/GitCore/LoadFromUrl.cs @@ -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); } /// @@ -113,8 +116,13 @@ public static async Task>> Lo Func?>? getBlobFromCache = null, Action>? 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); } /// @@ -215,7 +223,7 @@ private static (GitObjects.CommitObject commit, IReadOnlyDictionary - private static async Task>> LoadSubdirectoryContentsWithBloblessCloneAsync( + private static async Task>> LoadSubdirectoryContentsViaBloblessCloneAsync( string gitUrl, string commitSha, FilePath subdirectoryPath,