using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Configuration; using Mindforge.API.Exceptions; using Mindforge.API.Models; using Mindforge.API.Services.Interfaces; namespace Mindforge.API.Services { public class GiteaService : IGiteaService { private readonly HttpClient _httpClient; private readonly string _baseUrl; private readonly string _owner; private readonly string _repo; private readonly string _token; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; public GiteaService(HttpClient httpClient, IConfiguration configuration) { _httpClient = httpClient; var repoUrl = configuration["GITEA_REPO_URL"]; if (string.IsNullOrEmpty(repoUrl)) throw new InvalidOperationException("GITEA_REPO_URL is not set in configuration."); _token = configuration["GITEA_ACCESS_TOKEN"] ?? throw new InvalidOperationException("GITEA_ACCESS_TOKEN is not set in configuration."); // Parse: https://host/owner/repo or https://host/owner/repo.git var normalizedUrl = repoUrl.TrimEnd('/').TrimEnd(".git".ToCharArray()); var uri = new Uri(normalizedUrl); _baseUrl = $"{uri.Scheme}://{uri.Host}{(uri.IsDefaultPort ? "" : $":{uri.Port}")}"; var parts = uri.AbsolutePath.Trim('/').Split('/'); _owner = parts[0]; _repo = parts[1]; } public string GetRepositoryName() => _repo; public async Task> GetFileTreeAsync() { // Get the master branch to obtain the latest commit SHA var branchJson = await GetApiAsync($"/api/v1/repos/{_owner}/{_repo}/branches/master"); var branch = JsonSerializer.Deserialize(branchJson, JsonOptions) ?? throw new InvalidOperationException("Failed to parse branch response from Gitea."); var treeSha = branch.Commit.Id; // Fetch the full recursive tree var treeJson = await GetApiAsync($"/api/v1/repos/{_owner}/{_repo}/git/trees/{treeSha}?recursive=true"); var treeResponse = JsonSerializer.Deserialize(treeJson, JsonOptions) ?? throw new InvalidOperationException("Failed to parse tree response from Gitea."); return BuildTree(treeResponse.Tree); } public async Task GetFileContentAsync(string path) { var request = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}/api/v1/repos/{_owner}/{_repo}/raw/{path}?ref=master"); request.Headers.Add("Authorization", $"token {_token}"); var response = await _httpClient.SendAsync(request); if (response.StatusCode == System.Net.HttpStatusCode.NotFound) throw new UserException($"File not found in repository: {path}"); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } private async Task GetApiAsync(string endpoint) { var request = new HttpRequestMessage(HttpMethod.Get, $"{_baseUrl}{endpoint}"); request.Headers.Add("Authorization", $"token {_token}"); var response = await _httpClient.SendAsync(request); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } private static List BuildTree(List items) { var root = new List(); var folderMap = new Dictionary(); // Only include .md files and their parent folders var mdFiles = items.Where(i => i.Type == "blob" && i.Path.EndsWith(".md")).ToList(); var neededFolders = new HashSet(); foreach (var file in mdFiles) { var dir = System.IO.Path.GetDirectoryName(file.Path)?.Replace('\\', '/'); while (!string.IsNullOrEmpty(dir)) { neededFolders.Add(dir); dir = System.IO.Path.GetDirectoryName(dir)?.Replace('\\', '/'); } } // Create folder nodes foreach (var item in items.Where(i => i.Type == "tree" && neededFolders.Contains(i.Path)).OrderBy(i => i.Path)) { var node = new FileTreeNode { Name = System.IO.Path.GetFileName(item.Path), Path = item.Path, Type = "folder", Children = [] }; folderMap[item.Path] = node; } // Place file and folder nodes into parent var allNodes = mdFiles.Select(i => new FileTreeNode { Name = System.IO.Path.GetFileName(i.Path), Path = i.Path, Type = "file" }).Concat(folderMap.Values).OrderBy(n => n.Path); foreach (var node in allNodes) { var parentPath = System.IO.Path.GetDirectoryName(node.Path)?.Replace('\\', '/'); if (string.IsNullOrEmpty(parentPath)) root.Add(node); else if (folderMap.TryGetValue(parentPath, out var parent)) parent.Children!.Add(node); } return root; } // ---- Gitea API DTOs ---- private class GiteaBranch { [JsonPropertyName("commit")] public GiteaCommit Commit { get; set; } = new(); } private class GiteaCommit { [JsonPropertyName("id")] public string Id { get; set; } = ""; } private class GiteaTreeResponse { [JsonPropertyName("tree")] public List Tree { get; set; } = []; } private class GiteaTreeItem { [JsonPropertyName("path")] public string Path { get; set; } = ""; [JsonPropertyName("type")] public string Type { get; set; } = ""; // "blob" | "tree" } } }