diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Helpers/GitHelperTests.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Helpers/GitHelperTests.cs index ff979606a77..17a361709df 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Helpers/GitHelperTests.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli.Tests/Helpers/GitHelperTests.cs @@ -79,7 +79,90 @@ public void GetRepoRemoteUri_WithNonGitDirectory_ThrowsException() try { - Assert.Throws(() => gitHelper.GetRepoRemoteUri(tempDir)); + Assert.Throws(() => gitHelper.GetRepoRemoteUri(tempDir)); + } + finally + { + if (Directory.Exists(tempDir)) + { + Directory.Delete(tempDir, true); + } + } + } + + [Test] + public async Task GetRepoFullNameAsync_WithSubdirectoryPath_ReturnsCorrectFullName() + { + var testRepoPath = CreateTestRepoWithRemote("git@github.com:Azure/azure-rest-api-specs.git"); + var subDir = Path.Combine(testRepoPath, "subdirectory"); + Directory.CreateDirectory(subDir); + mockGitHubService.Setup(x => x.GetGitHubParentRepoUrlAsync("Azure", "azure-rest-api-specs")) + .ReturnsAsync(string.Empty); // Not a fork + + try + { + var result = await gitHelper.GetRepoFullNameAsync(subDir); + + Assert.That(result, Is.EqualTo("Azure/azure-rest-api-specs")); + } + finally + { + CleanupTestRepo(testRepoPath); + } + } + + [Test] + public async Task GetRepoFullNameAsync_WithForkRepoButDontFindUpstream_ReturnsDirectFullName() + { + var testRepoPath = CreateTestRepoWithRemote("https://github.com/UserFork/azure-rest-api-specs.git"); + + try + { + var result = await gitHelper.GetRepoFullNameAsync(testRepoPath, findUpstreamParent: false); + + Assert.That(result, Is.EqualTo("UserFork/azure-rest-api-specs")); + } + finally + { + CleanupTestRepo(testRepoPath); + } + } + + [Test] + public async Task GetRepoFullNameAsync_WithEmptyPath_ThrowsArgumentException() + { + // Test empty string + try + { + await gitHelper.GetRepoFullNameAsync(""); + Assert.Fail("Expected ArgumentException was not thrown"); + } + catch (ArgumentException ex) + { + Assert.That(ex.ParamName, Is.EqualTo("pathInRepo")); + } + + // Test null + try + { + await gitHelper.GetRepoFullNameAsync(null!); + Assert.Fail("Expected ArgumentException was not thrown"); + } + catch (ArgumentException ex) + { + Assert.That(ex.ParamName, Is.EqualTo("pathInRepo")); + } + } + + [Test] + public void GetRepoFullNameAsync_WithNonGitDirectory_ThrowsException() + { + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + + try + { + Assert.ThrowsAsync(async () => await gitHelper.GetRepoFullNameAsync(tempDir)); } finally { diff --git a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Helpers/GitHelper.cs b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Helpers/GitHelper.cs index 315462aa78c..98353b3e16a 100644 --- a/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Helpers/GitHelper.cs +++ b/tools/azsdk-cli/Azure.Sdk.Tools.Cli/Helpers/GitHelper.cs @@ -9,13 +9,13 @@ namespace Azure.Sdk.Tools.Cli.Helpers public interface IGitHelper { // Get the owner - public Task GetRepoOwnerNameAsync(string path, bool findUpstreamParent = true); - public Task GetRepoFullNameAsync(string path, bool findUpstreamParent = true); - public Uri GetRepoRemoteUri(string path); - public string GetBranchName(string path); - public string GetMergeBaseCommitSha(string path, string targetBranch); - public string DiscoverRepoRoot(string path); - public string GetRepoName(string path); + public Task GetRepoOwnerNameAsync(string pathInRepo, bool findUpstreamParent = true); + public Task GetRepoFullNameAsync(string pathInRepo, bool findUpstreamParent = true); + public Uri GetRepoRemoteUri(string pathInRepo); + public string GetBranchName(string pathInRepo); + public string GetMergeBaseCommitSha(string pathInRepo, string targetBranch); + public string DiscoverRepoRoot(string pathInRepo); + public string GetRepoName(string pathInRepo); } public class GitHelper(IGitHubService gitHubService, ILogger logger) : IGitHelper @@ -23,9 +23,16 @@ public class GitHelper(IGitHubService gitHubService, ILogger logger) private readonly ILogger logger = logger; private readonly IGitHubService gitHubService = gitHubService; - public string GetMergeBaseCommitSha(string path, string targetBranchName) + /// + /// Gets the SHA of the merge base (common ancestor) between the current branch and the target branch. + /// + /// Any path within the git repository (file or directory) + /// The name of the target branch to find the merge base with + /// The SHA of the merge base commit, or empty string if not found + public string GetMergeBaseCommitSha(string pathInRepo, string targetBranchName) { - using (var repo = new Repository(path)) + var repoRoot = DiscoverRepoRoot(pathInRepo); + using (var repo = new Repository(repoRoot)) { // Get the current branch Branch currentBranch = repo.Head; @@ -38,16 +45,29 @@ public string GetMergeBaseCommitSha(string path, string targetBranchName) } } - public string GetBranchName(string repoPath) + /// + /// Gets the friendly name of the current branch in the repository. + /// + /// Any path within the git repository (file or directory) + /// The friendly name of the current branch + public string GetBranchName(string pathInRepo) { - using var repo = new Repository(repoPath); + var repoRoot = DiscoverRepoRoot(pathInRepo); + using var repo = new Repository(repoRoot); var branchName = repo.Head.FriendlyName; return branchName; } - public Uri GetRepoRemoteUri(string path) + /// + /// Gets the remote origin URI of the repository in HTTPS format. + /// + /// Any path within the git repository (file or directory) + /// The HTTPS URI of the remote origin + /// Thrown when unable to determine remote URL + public Uri GetRepoRemoteUri(string pathInRepo) { - using var repo = new Repository(path); + var repoRoot = DiscoverRepoRoot(pathInRepo); + using var repo = new Repository(repoRoot); var remote = repo.Network?.Remotes["origin"]; if (remote != null) { @@ -87,9 +107,16 @@ private static string ConvertSshToHttpsUrl(string gitUrl) return gitUrl; } - public async Task GetRepoOwnerNameAsync(string path, bool findUpstreamParent = true) + /// + /// Gets the owner name of the repository, optionally finding the upstream parent if the repo is a fork. + /// + /// Any path within the git repository (file or directory) + /// Whether to find the upstream parent repo if this is a fork (default: true) + /// The owner name of the repository or its upstream parent + /// Thrown when unable to determine repository owner + public async Task GetRepoOwnerNameAsync(string pathInRepo, bool findUpstreamParent = true) { - var uri = GetRepoRemoteUri(path); + var uri = GetRepoRemoteUri(pathInRepo); var segments = uri.Segments; string repoOwner = string.Empty; string repoName = string.Empty; @@ -122,52 +149,74 @@ public async Task GetRepoOwnerNameAsync(string path, bool findUpstreamPa throw new InvalidOperationException("Unable to determine repository owner."); } - // Get the full name of repo in the format of "{owner/name}", e.g. "Azure/azure-rest-api-specs" - public async Task GetRepoFullNameAsync(string path, bool findUpstreamParent = true) + /// + /// Gets the full name of the repository in the format "{owner}/{name}", e.g. "Azure/azure-rest-api-specs". + /// + /// Any path within the git repository (file or directory) + /// Whether to find the upstream parent repo if this is a fork (default: true) + /// The full name of the repository in "owner/name" format + /// Thrown when pathInRepo is null or empty + public async Task GetRepoFullNameAsync(string pathInRepo, bool findUpstreamParent = true) { - if (!string.IsNullOrEmpty(path)) + if (!string.IsNullOrEmpty(pathInRepo)) { - var repoOwner = await GetRepoOwnerNameAsync(path, findUpstreamParent); - var repoName = GetRepoName(path); + var repoOwner = await GetRepoOwnerNameAsync(pathInRepo, findUpstreamParent); + var repoName = GetRepoName(pathInRepo); return $"{repoOwner}/{repoName}"; } - throw new ArgumentException("Invalid repository path.", nameof(path)); + throw new ArgumentException("Invalid repository path.", nameof(pathInRepo)); } - public string DiscoverRepoRoot(string path) + /// + /// Discovers and returns the root directory path of the git repository containing the specified path. + /// + /// Any path within the git repository (file or directory) + /// The absolute path to the repository root directory + /// Thrown when no git repository is found at or above the specified path + public string DiscoverRepoRoot(string pathInRepo) { - var repoPath = Repository.Discover(path); + // Discover the repo root for this path + var repoPath = Repository.Discover(pathInRepo); if (string.IsNullOrEmpty(repoPath)) { - throw new InvalidOperationException($"No git repository found at or above the path: {path}"); + throw new InvalidOperationException($"No git repository found at or above the path: {pathInRepo}"); } // Repository.Discover returns the path to .git directory // The repository root is the parent directory of .git var gitDir = new DirectoryInfo(repoPath); - return gitDir.Parent?.FullName ?? throw new InvalidOperationException("Unable to determine repository root"); + if (gitDir.Parent == null || string.IsNullOrEmpty(gitDir.Parent.FullName)) + { + throw new InvalidOperationException("Unable to determine repository root"); + } + + return gitDir.Parent.FullName; } - // Get the repository name from the local path - public string GetRepoName(string path) + /// + /// Gets the repository name from the remote origin URL. + /// + /// Any path within the git repository (file or directory) + /// The name of the repository (without the owner) + /// Thrown when pathInRepo is null or empty + /// Thrown when unable to determine repository name from remote URL + public string GetRepoName(string pathInRepo) { - if (string.IsNullOrEmpty(path)) + if (string.IsNullOrEmpty(pathInRepo)) { - throw new ArgumentException("Invalid repository path.", nameof(path)); + throw new ArgumentException("Invalid repository path.", nameof(pathInRepo)); } - var repoRoot = DiscoverRepoRoot(path); - var uri = GetRepoRemoteUri(repoRoot); + var uri = GetRepoRemoteUri(pathInRepo); var segments = uri.Segments; if (segments.Length < 2) { - throw new InvalidOperationException($"Unable to parse repository owner and name from remote URL: {uri}"); + throw new InvalidOperationException($"Unable to parse repository name from remote URL: {uri}"); } string repoName = segments[^1].TrimEnd(".git".ToCharArray()); - return repoName; } }