269 lines
10 KiB
C#
269 lines
10 KiB
C#
using System.ComponentModel;
|
||
using System.Text.Json;
|
||
using LazyBear.MCP.Services.ToolRegistry;
|
||
using ModelContextProtocol.Server;
|
||
using RestSharp;
|
||
|
||
namespace LazyBear.MCP.Services.GitLab;
|
||
|
||
public sealed class GitLabVersionTools(
|
||
GitLabClientProvider provider,
|
||
IConfiguration configuration,
|
||
ToolRegistryService registry)
|
||
{
|
||
private readonly string _token = configuration["GitLab:Token"] ?? string.Empty;
|
||
private readonly string _baseUrl = configuration["GitLab:Url"] ?? string.Empty;
|
||
private const string ModuleName = "GitLab";
|
||
|
||
private bool TryCheckEnabled(string toolName, out string error)
|
||
{
|
||
if (!registry.IsToolEnabled(ModuleName, toolName))
|
||
{
|
||
error = $"Инструмент '{toolName}' модуля GitLab отключён в TUI.";
|
||
return false;
|
||
}
|
||
error = string.Empty;
|
||
return true;
|
||
}
|
||
|
||
private bool TryGetClient(out RestSharp.IRestClient client, out string error)
|
||
{
|
||
if (provider.InitializationError is not null)
|
||
{
|
||
client = null!;
|
||
error = $"GitLab клиент не инициализирован. Детали: {provider.InitializationError}";
|
||
return false;
|
||
}
|
||
|
||
var clientInstance = provider.GetClient();
|
||
if (clientInstance is null)
|
||
{
|
||
client = null!;
|
||
error = "GitLab клиент не создан.";
|
||
return false;
|
||
}
|
||
|
||
client = clientInstance.RestClient;
|
||
error = string.Empty;
|
||
return true;
|
||
}
|
||
|
||
private string FormatResponseError(string toolName, RestResponse response, string? resource = null)
|
||
{
|
||
var resourcePart = string.IsNullOrWhiteSpace(resource) ? string.Empty : $", resource='{resource}'";
|
||
var body = string.IsNullOrWhiteSpace(response.Content) ? "-" : response.Content;
|
||
return $"Ошибка GitLab в tool '{toolName}'{resourcePart}: status={(int)response.StatusCode}, details={body}";
|
||
}
|
||
|
||
private string FormatException(string toolName, Exception exception, string? resource = null)
|
||
{
|
||
var resourcePart = string.IsNullOrWhiteSpace(resource) ? string.Empty : $", resource='{resource}'";
|
||
return $"Ошибка GitLab в tool '{toolName}'{resourcePart}: {exception.GetType().Name}: {exception.Message}";
|
||
}
|
||
|
||
private static string GetVisibility(string visibility) => visibility switch
|
||
{
|
||
"public" => "Public",
|
||
"internal" => "Internal",
|
||
"private" => "Private",
|
||
_ => visibility ?? "unknown"
|
||
};
|
||
|
||
private static string? GetNestedString(JsonElement element, params string[] path)
|
||
{
|
||
var current = element;
|
||
foreach (var segment in path)
|
||
{
|
||
if (!current.TryGetProperty(segment, out current))
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
return current.ValueKind == JsonValueKind.String ? current.GetString() : current.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить список тегов
|
||
/// </summary>
|
||
/// <param name="projectId">ID проекта</param>
|
||
/// <param name="cancellationToken">Token отмены</param>
|
||
[McpServerTool, Description("Получить список тегов проекта")]
|
||
public async Task<string> ListVersions(
|
||
[Description("ID проекта")] int projectId,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (!TryCheckEnabled("ListVersions", out var enabledError)) return enabledError;
|
||
|
||
if (projectId <= 0)
|
||
{
|
||
return "ID проекта GitLab некорректно задан.";
|
||
}
|
||
|
||
if (!TryGetClient(out var client, out var error)) return error;
|
||
|
||
try
|
||
{
|
||
var request = new RestRequest($"/projects/{projectId}/repository/tags", RestSharp.Method.Get);
|
||
request.AddHeader("Accept", "application/json");
|
||
request.AddHeader("PRIVATE-TOKEN", _token);
|
||
request.AddQueryParameter("per_page", "30");
|
||
|
||
var response = await client.ExecuteAsync(request, cancellationToken);
|
||
|
||
if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content))
|
||
{
|
||
return FormatResponseError("list_versions", response, $"/projects/{projectId}/repository/tags");
|
||
}
|
||
|
||
using var document = JsonDocument.Parse(response.Content);
|
||
var root = document.RootElement;
|
||
|
||
if (!root.TryGetProperty("tags", out var tagsElement) || tagsElement.GetArrayLength() == 0)
|
||
{
|
||
return $"Тегов в проекте #{projectId} не найдено.";
|
||
}
|
||
|
||
var lines = new List<string>();
|
||
foreach (var tag in tagsElement.EnumerateArray())
|
||
{
|
||
var name = GetNestedString(tag, "name") ?? "-";
|
||
var commitSha = GetNestedString(tag, "commit", "sha") ?? "-";
|
||
var commitMessage = GetNestedString(tag, "commit", "message") ?? "-";
|
||
var tagType = GetNestedString(tag, "tag_type") ?? "unknown";
|
||
|
||
lines.Add($"'{name}' (type={tagType}, sha={commitSha})\n message: {commitMessage}");
|
||
}
|
||
|
||
return $"Тег версии проекта #{projectId} ({tagsElement.GetArrayLength()} шт.):\n{string.Join('\n', lines)}";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return FormatException("list_versions", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Создать тег
|
||
/// </summary>
|
||
/// <param name="projectId">ID проекта</param>
|
||
/// <param name="name">Имя тега</param>
|
||
/// <param name="description">Описание (опционально)</param>
|
||
/// <param name="cancellationToken">Token отмены</param>
|
||
[McpServerTool, Description("Создать тег версии")]
|
||
public async Task<string> CreateVersion(
|
||
[Description("ID проекта")] int projectId,
|
||
[Description("Имя тега")] string name,
|
||
[Description("Описание тега")] string? description = null,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (!TryCheckEnabled("CreateVersion", out var enabledError)) return enabledError;
|
||
|
||
if (projectId <= 0)
|
||
{
|
||
return "ID проекта GitLab некорректно задан.";
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(name))
|
||
{
|
||
return "Имя тега GitLab не может быть пустым.";
|
||
}
|
||
|
||
if (!TryGetClient(out var client, out var error)) return error;
|
||
|
||
try
|
||
{
|
||
var request = new RestRequest($"/projects/{projectId}/repository/tags", RestSharp.Method.Post);
|
||
request.AddHeader("Accept", "application/json");
|
||
request.AddHeader("PRIVATE-TOKEN", _token);
|
||
request.AddHeader("Content-Type", "application/json");
|
||
|
||
var jsonBody = new
|
||
{
|
||
name = name,
|
||
description = description ?? string.Empty
|
||
}.ToJson();
|
||
request.AddJsonBody(jsonBody);
|
||
|
||
var response = await client.ExecuteAsync(request, cancellationToken);
|
||
|
||
if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content))
|
||
{
|
||
return FormatResponseError("create_version", response, $"/projects/{projectId}/repository/tags");
|
||
}
|
||
|
||
using var document = JsonDocument.Parse(response.Content);
|
||
var root = document.RootElement;
|
||
|
||
var tagName = GetNestedString(root, "name") ?? "-";
|
||
var sha = GetNestedString(root, "commit", "sha") ?? "-";
|
||
var refType = GetNestedString(root, "ref_type") ?? "-";
|
||
|
||
return $"Тег версии создан в проекте #{projectId}:\n'{tagName}' (ref_type={refType}, sha={sha})";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return FormatException("create_version", ex);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Удалить тег
|
||
/// </summary>
|
||
/// <param name="projectId">ID проекта</param>
|
||
/// <param name="tagName">Имя тега</param>
|
||
/// <param name="cancellationToken">Token отмены</param>
|
||
[McpServerTool, Description("Удалить тег версии")]
|
||
public async Task<string> DeleteVersion(
|
||
[Description("ID проекта")] int projectId,
|
||
[Description("Имя тега")] string tagName,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (!TryCheckEnabled("DeleteVersion", out var enabledError)) return enabledError;
|
||
|
||
if (projectId <= 0)
|
||
{
|
||
return "ID проекта GitLab некорректно задан.";
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(tagName))
|
||
{
|
||
return "Имя тега GitLab не может быть пустым.";
|
||
}
|
||
|
||
if (!TryGetClient(out var client, out var error)) return error;
|
||
|
||
try
|
||
{
|
||
var request = new RestRequest($"/projects/{projectId}/repository/tags/{tagName}", RestSharp.Method.Delete);
|
||
request.AddHeader("Accept", "application/json");
|
||
request.AddHeader("PRIVATE-TOKEN", _token);
|
||
|
||
var response = await client.ExecuteAsync(request, cancellationToken);
|
||
|
||
if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content))
|
||
{
|
||
return FormatResponseError("delete_version", response, $"/projects/{projectId}/repository/tags/{tagName}");
|
||
}
|
||
|
||
using var document = JsonDocument.Parse(response.Content);
|
||
var root = document.RootElement;
|
||
|
||
var deletedTag = GetNestedString(root, "name") ?? tagName;
|
||
|
||
return $"Тег '{deletedTag}' успешно удалён из проекта #{projectId}.";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return FormatException("delete_version", ex);
|
||
}
|
||
}
|
||
}
|
||
|
||
internal static class JsonExtensions
|
||
{
|
||
public static string ToJson(this object obj) =>
|
||
System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions
|
||
{
|
||
WriteIndented = false
|
||
});
|
||
} |