359 lines
14 KiB
C#
359 lines
14 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 GitLabBranchTools(
|
||
GitLabClientProvider provider,
|
||
IConfiguration configuration,
|
||
ToolRegistryService registry)
|
||
{
|
||
private readonly string _token = configuration["GitLab:Token"] ?? 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? 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();
|
||
}
|
||
|
||
[McpServerTool, Description("Получить список веток GitLab проекта")]
|
||
public async Task<string> ListBranches(
|
||
[Description("ID проекта")] int projectId,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (!TryCheckEnabled("ListBranches", 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/branches", Method.Get);
|
||
request.AddHeader("Accept", "application/json");
|
||
request.AddHeader("PRIVATE-TOKEN", _token);
|
||
request.AddQueryParameter("per_page", "100");
|
||
|
||
var response = await client.ExecuteAsync(request, cancellationToken);
|
||
if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content))
|
||
{
|
||
return FormatResponseError("list_branches", response, $"/projects/{projectId}/repository/branches");
|
||
}
|
||
|
||
using var document = JsonDocument.Parse(response.Content);
|
||
var root = document.RootElement;
|
||
|
||
if (root.ValueKind != JsonValueKind.Array || root.GetArrayLength() == 0)
|
||
{
|
||
return $"Ветки в проекте #{projectId} не найдены.";
|
||
}
|
||
|
||
var lines = new List<string>();
|
||
foreach (var branch in root.EnumerateArray())
|
||
{
|
||
var name = GetNestedString(branch, "name") ?? "-";
|
||
var isDefault = GetNestedString(branch, "default") ?? "false";
|
||
var isProtected = GetNestedString(branch, "protected") ?? "false";
|
||
var commit = GetNestedString(branch, "commit", "short_id") ?? GetNestedString(branch, "commit", "id") ?? "-";
|
||
|
||
lines.Add($"{name} (default={isDefault}, protected={isProtected}, commit={commit})");
|
||
}
|
||
|
||
return $"Ветки проекта #{projectId} ({root.GetArrayLength()} шт.):{Environment.NewLine}{string.Join(Environment.NewLine, lines)}";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return FormatException("list_branches", ex);
|
||
}
|
||
}
|
||
|
||
[McpServerTool, Description("Получить ветку GitLab проекта")]
|
||
public async Task<string> GetBranch(
|
||
[Description("ID проекта")] int projectId,
|
||
[Description("Имя ветки")] string branchName,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (!TryCheckEnabled("GetBranch", out var enabledError)) return enabledError;
|
||
|
||
if (projectId <= 0)
|
||
{
|
||
return "ID проекта GitLab некорректно задан.";
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(branchName))
|
||
{
|
||
return "Имя ветки GitLab не может быть пустым.";
|
||
}
|
||
|
||
if (!TryGetClient(out var client, out var error)) return error;
|
||
|
||
try
|
||
{
|
||
var encoded = Uri.EscapeDataString(branchName);
|
||
var request = new RestRequest($"/projects/{projectId}/repository/branches/{encoded}", Method.Get);
|
||
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("get_branch", response, $"/projects/{projectId}/repository/branches/{encoded}");
|
||
}
|
||
|
||
using var document = JsonDocument.Parse(response.Content);
|
||
var root = document.RootElement;
|
||
|
||
var name = GetNestedString(root, "name") ?? branchName;
|
||
var isDefault = GetNestedString(root, "default") ?? "false";
|
||
var isProtected = GetNestedString(root, "protected") ?? "false";
|
||
var canPush = GetNestedString(root, "can_push") ?? "false";
|
||
var commitId = GetNestedString(root, "commit", "id") ?? "-";
|
||
|
||
return $"Ветка '{name}' проекта #{projectId}:{Environment.NewLine}default={isDefault}, protected={isProtected}, can_push={canPush}{Environment.NewLine}commit={commitId}";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return FormatException("get_branch", ex);
|
||
}
|
||
}
|
||
|
||
[McpServerTool, Description("Создать ветку GitLab проекта")]
|
||
public async Task<string> CreateBranch(
|
||
[Description("ID проекта")] int projectId,
|
||
[Description("Имя новой ветки")] string branchName,
|
||
[Description("Ветка или SHA-реф источника")] string @ref,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (!TryCheckEnabled("CreateBranch", out var enabledError)) return enabledError;
|
||
|
||
if (projectId <= 0)
|
||
{
|
||
return "ID проекта GitLab некорректно задан.";
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(branchName))
|
||
{
|
||
return "Имя новой ветки GitLab не может быть пустым.";
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(@ref))
|
||
{
|
||
return "Источник ветки (ref) GitLab не может быть пустым.";
|
||
}
|
||
|
||
if (!TryGetClient(out var client, out var error)) return error;
|
||
|
||
try
|
||
{
|
||
var request = new RestRequest($"/projects/{projectId}/repository/branches", Method.Post);
|
||
request.AddHeader("Accept", "application/json");
|
||
request.AddHeader("PRIVATE-TOKEN", _token);
|
||
request.AddHeader("Content-Type", "application/json");
|
||
request.AddJsonBody(new { branch = branchName, @ref });
|
||
|
||
var response = await client.ExecuteAsync(request, cancellationToken);
|
||
if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content))
|
||
{
|
||
return FormatResponseError("create_branch", response, $"/projects/{projectId}/repository/branches");
|
||
}
|
||
|
||
using var document = JsonDocument.Parse(response.Content);
|
||
var root = document.RootElement;
|
||
var name = GetNestedString(root, "name") ?? branchName;
|
||
var commit = GetNestedString(root, "commit", "short_id") ?? GetNestedString(root, "commit", "id") ?? "-";
|
||
|
||
return $"Ветка '{name}' успешно создана в проекте #{projectId}. commit={commit}";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return FormatException("create_branch", ex);
|
||
}
|
||
}
|
||
|
||
[McpServerTool, Description("Удалить ветку GitLab проекта")]
|
||
public async Task<string> DeleteBranch(
|
||
[Description("ID проекта")] int projectId,
|
||
[Description("Имя ветки")] string branchName,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (!TryCheckEnabled("DeleteBranch", out var enabledError)) return enabledError;
|
||
|
||
if (projectId <= 0)
|
||
{
|
||
return "ID проекта GitLab некорректно задан.";
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(branchName))
|
||
{
|
||
return "Имя ветки GitLab не может быть пустым.";
|
||
}
|
||
|
||
if (!TryGetClient(out var client, out var error)) return error;
|
||
|
||
try
|
||
{
|
||
var encoded = Uri.EscapeDataString(branchName);
|
||
var request = new RestRequest($"/projects/{projectId}/repository/branches/{encoded}", Method.Delete);
|
||
request.AddHeader("Accept", "application/json");
|
||
request.AddHeader("PRIVATE-TOKEN", _token);
|
||
|
||
var response = await client.ExecuteAsync(request, cancellationToken);
|
||
if (!response.IsSuccessful)
|
||
{
|
||
return FormatResponseError("delete_branch", response, $"/projects/{projectId}/repository/branches/{encoded}");
|
||
}
|
||
|
||
return $"Ветка '{branchName}' успешно удалена из проекта #{projectId}.";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return FormatException("delete_branch", ex);
|
||
}
|
||
}
|
||
|
||
[McpServerTool, Description("Защитить ветку GitLab проекта")]
|
||
public async Task<string> ProtectBranch(
|
||
[Description("ID проекта")] int projectId,
|
||
[Description("Имя ветки")] string branchName,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (!TryCheckEnabled("ProtectBranch", out var enabledError)) return enabledError;
|
||
|
||
if (projectId <= 0)
|
||
{
|
||
return "ID проекта GitLab некорректно задан.";
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(branchName))
|
||
{
|
||
return "Имя ветки GitLab не может быть пустым.";
|
||
}
|
||
|
||
if (!TryGetClient(out var client, out var error)) return error;
|
||
|
||
try
|
||
{
|
||
var request = new RestRequest($"/projects/{projectId}/protected_branches", Method.Post);
|
||
request.AddHeader("Accept", "application/json");
|
||
request.AddHeader("PRIVATE-TOKEN", _token);
|
||
request.AddHeader("Content-Type", "application/json");
|
||
request.AddJsonBody(new { name = branchName });
|
||
|
||
var response = await client.ExecuteAsync(request, cancellationToken);
|
||
if (!response.IsSuccessful || string.IsNullOrWhiteSpace(response.Content))
|
||
{
|
||
return FormatResponseError("protect_branch", response, $"/projects/{projectId}/protected_branches");
|
||
}
|
||
|
||
return $"Ветка '{branchName}' успешно защищена в проекте #{projectId}.";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return FormatException("protect_branch", ex);
|
||
}
|
||
}
|
||
|
||
[McpServerTool, Description("Снять защиту с ветки GitLab проекта")]
|
||
public async Task<string> UnprotectBranch(
|
||
[Description("ID проекта")] int projectId,
|
||
[Description("Имя ветки")] string branchName,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
if (!TryCheckEnabled("UnprotectBranch", out var enabledError)) return enabledError;
|
||
|
||
if (projectId <= 0)
|
||
{
|
||
return "ID проекта GitLab некорректно задан.";
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(branchName))
|
||
{
|
||
return "Имя ветки GitLab не может быть пустым.";
|
||
}
|
||
|
||
if (!TryGetClient(out var client, out var error)) return error;
|
||
|
||
try
|
||
{
|
||
var encoded = Uri.EscapeDataString(branchName);
|
||
var request = new RestRequest($"/projects/{projectId}/protected_branches/{encoded}", Method.Delete);
|
||
request.AddHeader("Accept", "application/json");
|
||
request.AddHeader("PRIVATE-TOKEN", _token);
|
||
|
||
var response = await client.ExecuteAsync(request, cancellationToken);
|
||
if (!response.IsSuccessful)
|
||
{
|
||
return FormatResponseError("unprotect_branch", response, $"/projects/{projectId}/protected_branches/{encoded}");
|
||
}
|
||
|
||
return $"Защита с ветки '{branchName}' успешно снята в проекте #{projectId}.";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
return FormatException("unprotect_branch", ex);
|
||
}
|
||
}
|
||
}
|