From 5c7ff44e023183dda73ebab78430d3d06dbed65a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:58:35 +0000 Subject: [PATCH 1/4] Initial plan From 5526d9a03858affffc4b8de07961cbcb1d6b1295 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 10 Nov 2025 12:04:58 +0000 Subject: [PATCH 2/4] Fix ParseTreeUrl and LoadTreeContentsFromUrlAsync to support subdirectories Co-authored-by: Viir <19209696+Viir@users.noreply.github.com> --- .../LoadTreeContentsFromGitHubTests.cs | 57 +++++++++ .../GitCore.UnitTests/ParseTreeUrlTests.cs | 111 ++++++++++++++++++ implement/GitCore/GitSmartHttp.cs | 33 ++++-- implement/GitCore/LoadFromUrl.cs | 12 ++ 4 files changed, 200 insertions(+), 13 deletions(-) create mode 100644 implement/GitCore.UnitTests/ParseTreeUrlTests.cs diff --git a/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs b/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs index 47e6531..38fea35 100644 --- a/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs +++ b/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs @@ -60,6 +60,63 @@ 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_branch() + { + // Test loading a subdirectory using a URL with branch name + var url = "https://github.com/Viir/GitCore/tree/main/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().BeGreaterThan(0, "Subdirectory should contain files"); + + // Verify specific files exist in the subdirectory + subdirectoryContents.Keys.Should().Contain(key => key.SequenceEqual(new[] { "GitObjects.cs" }), + "Should contain GitObjects.cs"); + + subdirectoryContents.Keys.Should().Contain(key => key.SequenceEqual(new[] { "LoadFromUrl.cs" }), + "Should contain LoadFromUrl.cs"); + + // Verify there's a file in the Common subdirectory + var hasCommonSubdir = subdirectoryContents.Keys + .Any(path => path.Count >= 2 && path[0] == "Common" && path[1] == "EnumerableExtensions.cs"); + + hasCommonSubdir.Should().BeTrue("Should contain files in the Common subdirectory"); + + // Verify we have the expected number of entries for this subdirectory + // The implement/GitCore directory has about 9 files (including Common/EnumerableExtensions.cs) + subdirectoryContents.Count.Should().Be(9, "Subdirectory should contain exactly 9 files"); + } + + [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().BeGreaterThan(0, "Subdirectory should contain files"); + + // Verify specific files exist in the subdirectory + subdirectoryContents.Keys.Should().Contain(key => key.SequenceEqual(new[] { "GitObjects.cs" }), + "Should contain GitObjects.cs"); + + subdirectoryContents.Keys.Should().Contain(key => key.SequenceEqual(new[] { "LoadFromUrl.cs" }), + "Should contain LoadFromUrl.cs"); + + // Verify we have the expected number of entries for this subdirectory + // At this specific commit, the directory may have a different count + subdirectoryContents.Count.Should().BeLessThan(15, "Subdirectory should not contain entire repository"); + subdirectoryContents.Count.Should().BeGreaterThan(3, "Subdirectory should contain multiple files"); + } + [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..25fba5d 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..4f1ceb7 100644 --- a/implement/GitCore/LoadFromUrl.cs +++ b/implement/GitCore/LoadFromUrl.cs @@ -46,6 +46,18 @@ await GitSmartHttp.FetchBranchCommitShaAsync( httpClient); } + // Check if a subdirectory is specified + if (parsed.SubdirectoryPath is not null && parsed.SubdirectoryPath.Count > 0) + { + // Load only the subdirectory contents + var gitUrl = $"{parsed.BaseUrl}/{parsed.Owner}/{parsed.Repo}.git"; + return await LoadSubdirectoryContentsFromGitUrlAsync( + gitUrl, + commitSha, + parsed.SubdirectoryPath, + httpClient); + } + // Fetch the pack file containing the commit and its tree var packFileData = await GitSmartHttp.FetchPackFileAsync(parsed.BaseUrl, parsed.Owner, parsed.Repo, commitSha, httpClient); From 2ae37e720b84401d336941e297254ce787cf715a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20R=C3=A4tzel?= Date: Mon, 10 Nov 2025 12:17:46 +0000 Subject: [PATCH 3/4] cleanup format --- .../LoadTreeContentsFromGitHubTests.cs | 38 +------------------ implement/GitCore/GitSmartHttp.cs | 4 +- implement/GitCore/LoadFromUrl.cs | 12 ++++-- 3 files changed, 12 insertions(+), 42 deletions(-) diff --git a/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs b/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs index 38fea35..df2b24e 100644 --- a/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs +++ b/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs @@ -60,37 +60,6 @@ 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_branch() - { - // Test loading a subdirectory using a URL with branch name - var url = "https://github.com/Viir/GitCore/tree/main/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().BeGreaterThan(0, "Subdirectory should contain files"); - - // Verify specific files exist in the subdirectory - subdirectoryContents.Keys.Should().Contain(key => key.SequenceEqual(new[] { "GitObjects.cs" }), - "Should contain GitObjects.cs"); - - subdirectoryContents.Keys.Should().Contain(key => key.SequenceEqual(new[] { "LoadFromUrl.cs" }), - "Should contain LoadFromUrl.cs"); - - // Verify there's a file in the Common subdirectory - var hasCommonSubdir = subdirectoryContents.Keys - .Any(path => path.Count >= 2 && path[0] == "Common" && path[1] == "EnumerableExtensions.cs"); - - hasCommonSubdir.Should().BeTrue("Should contain files in the Common subdirectory"); - - // Verify we have the expected number of entries for this subdirectory - // The implement/GitCore directory has about 9 files (including Common/EnumerableExtensions.cs) - subdirectoryContents.Count.Should().Be(9, "Subdirectory should contain exactly 9 files"); - } - [Fact] public async Task Load_subdirectory_tree_from_url_with_commit_sha() { @@ -102,7 +71,7 @@ public async Task Load_subdirectory_tree_from_url_with_commit_sha() // Verify that the subdirectory was loaded successfully subdirectoryContents.Should().NotBeNull("Subdirectory should be loaded"); - subdirectoryContents.Count.Should().BeGreaterThan(0, "Subdirectory should contain files"); + subdirectoryContents.Count.Should().Be(9, "Subdirectory should contain 9 files"); // Verify specific files exist in the subdirectory subdirectoryContents.Keys.Should().Contain(key => key.SequenceEqual(new[] { "GitObjects.cs" }), @@ -110,11 +79,6 @@ public async Task Load_subdirectory_tree_from_url_with_commit_sha() subdirectoryContents.Keys.Should().Contain(key => key.SequenceEqual(new[] { "LoadFromUrl.cs" }), "Should contain LoadFromUrl.cs"); - - // Verify we have the expected number of entries for this subdirectory - // At this specific commit, the directory may have a different count - subdirectoryContents.Count.Should().BeLessThan(15, "Subdirectory should not contain entire repository"); - subdirectoryContents.Count.Should().BeGreaterThan(3, "Subdirectory should contain multiple files"); } [Fact] diff --git a/implement/GitCore/GitSmartHttp.cs b/implement/GitCore/GitSmartHttp.cs index 25fba5d..d5f335e 100644 --- a/implement/GitCore/GitSmartHttp.cs +++ b/implement/GitCore/GitSmartHttp.cs @@ -43,7 +43,7 @@ public static ParseTreeUrlResult ParseTreeUrl(string url) { // Format: github.com/owner/repo/tree/commit-sha-or-branch[/subdirectory/path] var subdirectoryPath = pathParts.Length > 4 ? pathParts[4..] : null; - + return new ParseTreeUrlResult( BaseUrl: $"{scheme}://{host}", Owner: pathParts[0], @@ -56,7 +56,7 @@ public static ParseTreeUrlResult ParseTreeUrl(string url) { // Format: gitlab.com/owner/repo/-/tree/commit-sha-or-branch[/subdirectory/path] var subdirectoryPath = pathParts.Length > 5 ? pathParts[5..] : null; - + return new ParseTreeUrlResult( BaseUrl: $"{scheme}://{host}", Owner: pathParts[0], diff --git a/implement/GitCore/LoadFromUrl.cs b/implement/GitCore/LoadFromUrl.cs index 4f1ceb7..d633431 100644 --- a/implement/GitCore/LoadFromUrl.cs +++ b/implement/GitCore/LoadFromUrl.cs @@ -51,6 +51,7 @@ await GitSmartHttp.FetchBranchCommitShaAsync( { // Load only the subdirectory contents var gitUrl = $"{parsed.BaseUrl}/{parsed.Owner}/{parsed.Repo}.git"; + return await LoadSubdirectoryContentsFromGitUrlAsync( gitUrl, commitSha, @@ -125,8 +126,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); } /// @@ -227,7 +233,7 @@ private static (GitObjects.CommitObject commit, IReadOnlyDictionary - private static async Task>> LoadSubdirectoryContentsWithBloblessCloneAsync( + private static async Task>> LoadSubdirectoryContentsViaBloblessCloneAsync( string gitUrl, string commitSha, FilePath subdirectoryPath, From d3e8b134681fc32b2f7ab4cc50e4efe0673293da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20R=C3=A4tzel?= Date: Mon, 10 Nov 2025 12:21:12 +0000 Subject: [PATCH 4/4] avoid redundant --- .../LoadTreeContentsFromGitHubTests.cs | 6 ++--- implement/GitCore/LoadFromUrl.cs | 24 ++++++------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs b/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs index df2b24e..af9aa2e 100644 --- a/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs +++ b/implement/GitCore.IntegrationTests/LoadTreeContentsFromGitHubTests.cs @@ -74,11 +74,9 @@ public async Task Load_subdirectory_tree_from_url_with_commit_sha() subdirectoryContents.Count.Should().Be(9, "Subdirectory should contain 9 files"); // Verify specific files exist in the subdirectory - subdirectoryContents.Keys.Should().Contain(key => key.SequenceEqual(new[] { "GitObjects.cs" }), - "Should contain GitObjects.cs"); + subdirectoryContents.Should().ContainKey(["GitObjects.cs"]); - subdirectoryContents.Keys.Should().Contain(key => key.SequenceEqual(new[] { "LoadFromUrl.cs" }), - "Should contain LoadFromUrl.cs"); + subdirectoryContents.Should().ContainKey(["LoadFromUrl.cs"]); } [Fact] diff --git a/implement/GitCore/LoadFromUrl.cs b/implement/GitCore/LoadFromUrl.cs index d633431..ef48b4e 100644 --- a/implement/GitCore/LoadFromUrl.cs +++ b/implement/GitCore/LoadFromUrl.cs @@ -46,24 +46,14 @@ await GitSmartHttp.FetchBranchCommitShaAsync( httpClient); } - // Check if a subdirectory is specified - if (parsed.SubdirectoryPath is not null && parsed.SubdirectoryPath.Count > 0) - { - // Load only the subdirectory contents - var gitUrl = $"{parsed.BaseUrl}/{parsed.Owner}/{parsed.Repo}.git"; - - return await LoadSubdirectoryContentsFromGitUrlAsync( - gitUrl, - commitSha, - parsed.SubdirectoryPath, - 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); } ///