// Copyright (c) Microsoft Corporation. All rights reserved. // GithubService.cs using System.Text; using Azure.Storage.Files.Shares; using DevTeam.Options; using Microsoft.Extensions.Options; using Octokit; using Octokit.Helpers; namespace DevTeam.Backend; public class GithubService : IManageGithub { private readonly GitHubClient _ghClient; private readonly AzureOptions _azSettings; private readonly ILogger _logger; private readonly HttpClient _httpClient; public GithubService(IOptions azOptions, GitHubClient ghClient, ILogger logger, HttpClient httpClient) { ArgumentNullException.ThrowIfNull(azOptions); ArgumentNullException.ThrowIfNull(ghClient); ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(httpClient); _ghClient = ghClient; _azSettings = azOptions.Value; _logger = logger; _httpClient = httpClient; } public async Task CommitToBranch(string org, string repo, long parentNumber, long issueNumber, string rootDir, string branch) { try { var connectionString = $"DefaultEndpointsProtocol=https;AccountName={_azSettings.FilesAccountName};AccountKey={_azSettings.FilesAccountKey};EndpointSuffix=core.windows.net"; var dirName = $"{rootDir}/{org}-{repo}/{parentNumber}/{issueNumber}"; var share = new ShareClient(connectionString, _azSettings.FilesShareName); var directory = share.GetDirectoryClient(dirName); var remaining = new Queue(); remaining.Enqueue(directory); while (remaining.Count > 0) { var dir = remaining.Dequeue(); await foreach (var item in dir.GetFilesAndDirectoriesAsync()) { if (!item.IsDirectory && item.Name != "run.sh") // we don't want the generated script in the PR { try { var file = dir.GetFileClient(item.Name); var filePath = file.Path.Replace($"{_azSettings.FilesShareName}/", "", StringComparison.OrdinalIgnoreCase) .Replace($"{dirName}/", "", StringComparison.OrdinalIgnoreCase); var fileStream = await file.OpenReadAsync(); using (var reader = new StreamReader(fileStream, Encoding.UTF8)) { var value = await reader.ReadToEndAsync(); try { // Check if the file exists var existingFiles = await _ghClient.Repository.Content.GetAllContentsByRef(org, repo, filePath, branch); var existingFile = existingFiles[0]; // If the file exists, update it var updateChangeSet = await _ghClient.Repository.Content.UpdateFile( org, repo, filePath, new UpdateFileRequest("Updated file via AI", value, existingFile.Sha, branch)); // TODO: add more meaningful commit message } catch (NotFoundException) { // If the file doesn't exist, create it var createChangeSet = await _ghClient.Repository.Content.CreateFile( org, repo, filePath, new CreateFileRequest("Created file via AI", value, branch)); // TODO: add more meaningful commit message } catch (Exception ex) { _logger.LogError(ex, "Error while uploading file '{FileName}'.", item.Name); } } } catch (Exception ex) { _logger.LogError(ex, "Error while uploading file '{FileName}'.", item.Name); } } else if (item.IsDirectory) { remaining.Enqueue(dir.GetSubdirectoryClient(item.Name)); } } } } catch (Exception ex) { _logger.LogError(ex, "Error committing to branch"); throw; } } public async Task CreateBranch(string org, string repo, string branch) { try { var ghRepo = await _ghClient.Repository.Get(org, repo); var contents = await _ghClient.Repository.Content.GetAllContents(org, repo); if (!contents.Any()) { // Create a new file and commit it to the repository var createChangeSet = await _ghClient.Repository.Content.CreateFile( org, repo, "README.md", new CreateFileRequest("Initial commit", "# Readme") ); } await _ghClient.Git.Reference.CreateBranch(org, repo, branch, ghRepo.DefaultBranch); } catch (Exception ex) { _logger.LogError(ex, "Error creating branch"); throw; } } public async Task GetMainLanguage(string org, string repo) { try { var languages = await _ghClient.Repository.GetAllLanguages(org, repo); var mainLanguage = languages.OrderByDescending(l => l.NumberOfBytes).First(); return mainLanguage.Name; } catch (Exception ex) { _logger.LogError(ex, "Error getting main language"); throw; } } public async Task CreateIssue(string org, string repo, string input, string function, long parentNumber) { try { var newIssue = new NewIssue($"{function} chain for #{parentNumber}") { Body = input, }; newIssue.Labels.Add(function); newIssue.Labels.Add($"Parent.{parentNumber}"); var issue = await _ghClient.Issue.Create(org, repo, newIssue); return issue.Number; } catch (Exception ex) { _logger.LogError(ex, "Error creating issue"); throw; } } public async Task CreatePR(string org, string repo, long number, string branch) { try { var ghRepo = await _ghClient.Repository.Get(org, repo); await _ghClient.PullRequest.Create(org, repo, new NewPullRequest($"New app #{number}", branch, ghRepo.DefaultBranch)); } catch (Exception ex) { _logger.LogError(ex, "Error creating PR"); throw; } } public async Task PostComment(string org, string repo, long issueNumber, string comment) { try { await _ghClient.Issue.Comment.Create(org, repo, (int)issueNumber, comment); } catch (Exception ex) { _logger.LogError(ex, "Error posting comment"); throw; } } public async Task> GetFiles(string org, string repo, string branch, Func filter) { ArgumentNullException.ThrowIfNull(filter); try { var items = await _ghClient.Repository.Content.GetAllContentsByRef(org, repo, branch); return await CollectFiles(org, repo, branch, items, filter); } catch (Exception ex) { _logger.LogError(ex, "Error getting files"); throw; } } private async Task> CollectFiles(string org, string repo, string branch, IReadOnlyList items, Func filter) { try { var result = new List(); foreach (var item in items) { if (item.Type == ContentType.File && filter(item)) { var content = await _httpClient.GetStringAsync(new Uri(item.DownloadUrl)); result.Add(new FileResponse { Name = item.Name, Content = content }); } else if (item.Type == ContentType.Dir) { var subItems = await _ghClient.Repository.Content.GetAllContentsByRef(org, repo, item.Path, branch); result.AddRange(await CollectFiles(org, repo, branch, subItems, filter)); } } return result; } catch (Exception ex) { _logger.LogError(ex, "Error collecting files"); throw; } } } public class FileResponse { public required string Name { get; set; } public required string Content { get; set; } } public interface IManageGithub { Task CreateIssue(string org, string repo, string input, string functionName, long parentNumber); Task CreatePR(string org, string repo, long number, string branch); Task CreateBranch(string org, string repo, string branch); Task CommitToBranch(string org, string repo, long parentNumber, long issueNumber, string rootDir, string branch); Task PostComment(string org, string repo, long issueNumber, string comment); Task> GetFiles(string org, string repo, string branch, Func filter); Task GetMainLanguage(string org, string repo); }