adding gitea service

This commit is contained in:
2026-03-26 19:36:25 -03:00
parent 76cdb9654e
commit 83b1cb397d
16 changed files with 658 additions and 166 deletions

View File

@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
using Mindforge.API.Services.Interfaces;
namespace Mindforge.API.Controllers
{
[ApiController]
[Route("api/v1/repository")]
public class RepositoryController : ControllerBase
{
private readonly IGiteaService _giteaService;
public RepositoryController(IGiteaService giteaService)
{
_giteaService = giteaService;
}
[HttpGet("info")]
public IActionResult GetInfo()
{
return Ok(new { name = _giteaService.GetRepositoryName() });
}
[HttpGet("tree")]
public async Task<IActionResult> GetTree()
{
var tree = await _giteaService.GetFileTreeAsync();
return Ok(tree);
}
[HttpGet("file")]
public async Task<IActionResult> GetFile([FromQuery] string path)
{
if (string.IsNullOrWhiteSpace(path))
return BadRequest(new { error = "path query parameter is required." });
var content = await _giteaService.GetFileContentAsync(path);
return Ok(new { path, content });
}
}
}

View File

@@ -0,0 +1,10 @@
namespace Mindforge.API.Models
{
public class FileTreeNode
{
public string Name { get; set; } = "";
public string Path { get; set; } = "";
public string Type { get; set; } = ""; // "file" | "folder"
public List<FileTreeNode>? Children { get; set; }
}
}

View File

@@ -7,6 +7,9 @@ using Mindforge.API.Services.Interfaces;
var builder = WebApplication.CreateBuilder(args);
// Ensure environment variables are loaded into IConfiguration
builder.Configuration.AddEnvironmentVariables();
// Add services to the container.
builder.Services.AddControllers();
builder.Logging.AddConsole();
@@ -37,6 +40,7 @@ builder.Services.AddScoped<ILlmApiProvider, GeminiApiProvider>();
builder.Services.AddScoped<IAgentService, AgentService>();
builder.Services.AddScoped<IFileService, FileService>();
builder.Services.AddScoped<IFlashcardService, FlashcardService>();
builder.Services.AddScoped<IGiteaService, GiteaService>();
var app = builder.Build();
@@ -68,4 +72,17 @@ if (string.IsNullOrEmpty(geminiKey))
app.Logger.LogWarning("GEMINI_API_KEY not found in configuration.");
}
var giteaRepoUrl = builder.Configuration["GITEA_REPO_URL"];
var giteaAccessToken = builder.Configuration["GITEA_ACCESS_TOKEN"];
if (string.IsNullOrEmpty(giteaRepoUrl))
{
app.Logger.LogWarning("GITEA_REPO_URL not found in configuration. Repository features will not work.");
}
if (string.IsNullOrEmpty(giteaAccessToken))
{
app.Logger.LogWarning("GITEA_ACCESS_TOKEN not found in configuration. Repository features will not work.");
}
app.Run();

View File

@@ -0,0 +1,165 @@
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<List<FileTreeNode>> 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<GiteaBranch>(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<GiteaTreeResponse>(treeJson, JsonOptions)
?? throw new InvalidOperationException("Failed to parse tree response from Gitea.");
return BuildTree(treeResponse.Tree);
}
public async Task<string> 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<string> 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<FileTreeNode> BuildTree(List<GiteaTreeItem> items)
{
var root = new List<FileTreeNode>();
var folderMap = new Dictionary<string, FileTreeNode>();
// 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<string>();
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<GiteaTreeItem> Tree { get; set; } = [];
}
private class GiteaTreeItem
{
[JsonPropertyName("path")]
public string Path { get; set; } = "";
[JsonPropertyName("type")]
public string Type { get; set; } = ""; // "blob" | "tree"
}
}
}

View File

@@ -0,0 +1,11 @@
using Mindforge.API.Models;
namespace Mindforge.API.Services.Interfaces
{
public interface IGiteaService
{
Task<List<FileTreeNode>> GetFileTreeAsync();
Task<string> GetFileContentAsync(string path);
string GetRepositoryName();
}
}

View File

@@ -5,5 +5,9 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"OPENAI_API_KEY": "",
"GEMINI_API_KEY": "",
"GITEA_REPO_URL": "",
"GITEA_ACCESS_TOKEN": ""
}

View File

@@ -30,6 +30,16 @@ spec:
secretKeyRef:
name: mindforge-secrets
key: GEMINI_API_KEY
- name: GITEA_REPO_URL
valueFrom:
secretKeyRef:
name: mindforge-secrets
key: GITEA_REPO_URL
- name: GITEA_ACCESS_TOKEN
valueFrom:
secretKeyRef:
name: mindforge-secrets
key: GITEA_ACCESS_TOKEN
resources:
requests:
memory: "128Mi"