tools/mcp/dotnet/AzureSDKDevToolsMCP/Services/GitHubService.cs (180 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Octokit; using Octokit.Models; using Octokit.Clients; using System.Runtime.InteropServices; using System.Diagnostics; using Microsoft.Extensions.Logging; using AzureSDKDSpecTools.Services; namespace AzureSDKDevToolsMCP.Services { public interface IGitHubService { public Task<User> GetGitUserDetails(); public Task<IReadOnlyList<CheckRun>> GetPullRequestChecksAsync(string repoOwner, string repoName, int pullRequestNumber); public Task<PullRequest> GetPullRequestAsync(string repoOwner, string repoName, int pullRequestNumber); public Task<string> GetGitHubParentRepoUrl(string owner, string repoName); public Task<List<string>> CreatePullRequest(string repoName, string repoOwner, string baseBranch, string headBranch, string title, string body); public Task<List<string>> GetPullRequestCommentsAsync(string repoOwner, string repoName, int pullRequestNumber); public Task<PullRequest?> GetPullRequestForBranchAsync(string repoOwner, string repoName, string remoteBranch); } public class GitHubService : IGitHubService { private GitHubClient gitHubClient; private ILogger<GitHubService> logger; public GitHubService(ILogger<GitHubService> _logger) { logger = _logger; var token = GetGitHubAuthToken(); gitHubClient = new GitHubClient(new ProductHeaderValue("AzureSDKDevToolsMCP")) { Credentials = new Credentials(token, AuthenticationType.Bearer) }; } public async Task<User> GetGitUserDetails() { var user = await gitHubClient.User.Current(); return user; } public async Task<IReadOnlyList<CheckRun>> GetPullRequestChecksAsync(string repoOwner, string repoName, int pullRequestNumber) { var pr = await GetPullRequestAsync(repoOwner, repoName, pullRequestNumber) ?? throw new InvalidOperationException($"Pull request {pullRequestNumber} not found."); var checks = await gitHubClient.Check.Run.GetAllForReference(repoOwner, repoName, pr.Head.Sha); return checks == null ? throw new InvalidOperationException($"Check runs for pull request {pullRequestNumber} not found.") : checks.CheckRuns; } public async Task<PullRequest> GetPullRequestAsync(string repoOwner, string repoName, int pullRequestNumber) { var pullRequest = await gitHubClient.PullRequest.Get(repoOwner, repoName, pullRequestNumber); return pullRequest; } private static string GetGitHubAuthToken() { bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); string command = isWindows ? "cmd.exe" : "gh"; string args = isWindows ? "/C gh auth token" : "auth token"; var processStartInfo = new ProcessStartInfo { FileName = command, Arguments = args, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; using (var process = new Process()) { process.StartInfo = processStartInfo; process.Start(); string output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); if (process.ExitCode != 0) { throw new InvalidOperationException($"Failed to get GitHub auth token. Error: {process.StandardError.ReadToEnd()}"); } return output.Trim(); } } public async Task<string> GetGitHubParentRepoUrl(string owner, string repoName) { var repository = await gitHubClient.Repository.Get(owner, repoName); if (repository == null) { throw new InvalidOperationException($"Repository {owner}/{repoName} not found in GitHub."); } return repository.Parent?.Url ?? repository.Url; } public async Task<PullRequest?> GetPullRequestForBranchAsync(string repoOwner, string repoName, string remoteBranch) { logger.LogInformation($"Getting all pull request for {repoOwner}/{repoName}"); var pullRequests = await gitHubClient.PullRequest.GetAllForRepository(repoOwner, repoName); logger.LogInformation($"Branch name: {remoteBranch}"); return pullRequests?.FirstOrDefault(pr => pr.Head?.Label != null && pr.Head.Label.Equals(remoteBranch, StringComparison.InvariantCultureIgnoreCase)); } private async Task<bool> IsDiffMergeable(string targetRepoOwner, string repoName, string baseBranch, string headBranch) { logger.LogInformation("Comparing the headbranch against target branch"); var comparison = await gitHubClient.Repository.Commit.Compare(targetRepoOwner, repoName, baseBranch, headBranch); logger.LogInformation($"Comparison: {comparison.Status}"); return comparison?.MergeBaseCommit != null; } public async Task<List<string>> CreatePullRequest(string repoName, string repoOwner, string baseBranch, string headBranch, string title, string body) { var responseList = new List<string>(); // Check if a pull request already exists for the branch try { var pr = await GetPullRequestForBranchAsync(repoOwner, repoName, headBranch); if (pr != null) { responseList.Add($"Pull request already exists for branch {headBranch} in repository {repoOwner}/{repoName}. Pull request URL: {pr.HtmlUrl}"); return responseList; } responseList.Add($"No pull request found for branch {headBranch} in repository {repoOwner}/{repoName}. Proceeding to create a new pull request."); } catch (Exception ex) { logger.LogError(ex.Message); responseList.Add($"Failed to check for existing pull request for the branch. Error: {ex.Message}"); return responseList; } // Check mergeability of the branches try { responseList.Add($"Checking if changes are mergeable to {baseBranch} branch in repository [{repoOwner}/{repoName}]..."); var isMergeable = await IsDiffMergeable(repoOwner, repoName, baseBranch, headBranch); if (!isMergeable) { responseList.Add($"Changes from [{repoOwner}] are not mergeable to {baseBranch} branch in repository [{repoOwner}/{repoName}]. Please resolve the conflicts and try again."); responseList.Add($"By default, target branch in main. If you are trying to create a pull request to a different branch, please specify the target branch and try again."); return responseList; } } catch (Exception ex) { responseList.Add($"Failed to check if changes are mergeable to {baseBranch} branch in repository [{repoOwner}/{repoName}]. Error: {ex.Message}"); return responseList; } // Create the pull request responseList.Add($"Changes are mergeable. Proceeding to create pull request for changes in {headBranch}."); var pullRequest = new NewPullRequest(title, headBranch, baseBranch) { Body = body }; try { var createdPullRequest = await gitHubClient.PullRequest.Create(repoOwner, repoName, pullRequest); if (createdPullRequest == null) responseList.Add($"Failed to create pull request for changes in {headBranch}."); else responseList.Add($"Pull request created successfully. Pull request URL: {createdPullRequest.HtmlUrl}"); return responseList; } catch (Exception ex) { responseList.Add($"Failed to create pull request. Error: {ex.Message}"); return responseList; } } public async Task<List<string>> GetPullRequestCommentsAsync(string repoOwner, string repoName, int pullRequestNumber) { List<string> responseList = new List<string>(); try { var comments = await gitHubClient.Issue.Comment.GetAllForIssue(repoOwner, repoName, pullRequestNumber); if (comments == null || comments.Count == 0) { responseList.Add($"No comments found for pull request {pullRequestNumber}."); return responseList; } foreach (var comment in comments) { responseList.Add($"Comment by {comment.User.Login}: {comment.Body}"); } return responseList; } catch (Exception ex) { responseList.Add($"Failed to get comments for pull request {pullRequestNumber}. Error: {ex.Message}"); return responseList; } } } }