231 lines
9.3 KiB
C#
231 lines
9.3 KiB
C#
using LibGit2Sharp;
|
||
using PerfReviewSummarizer.Api.Models;
|
||
|
||
namespace PerfReviewSummarizer.Api.Services;
|
||
|
||
public class GitService : IGitService
|
||
{
|
||
public async Task<List<CommitInfo>> GetCommitsAsync(string repositoryPath, DateTime? startDate, DateTime? endDate)
|
||
{
|
||
return await Task.Run(() =>
|
||
{
|
||
var commits = new List<CommitInfo>();
|
||
|
||
if (!Repository.IsValid(repositoryPath))
|
||
{
|
||
throw new DirectoryNotFoundException($"Неверный путь к репозиторию: {repositoryPath}");
|
||
}
|
||
|
||
using var repo = new Repository(repositoryPath);
|
||
var commitFilter = new CommitFilter
|
||
{
|
||
SortBy = CommitSortStrategies.Time | CommitSortStrategies.Reverse
|
||
};
|
||
|
||
foreach (var commit in repo.Commits.QueryBy(commitFilter))
|
||
{
|
||
var commitDate = commit.Author.When.DateTime;
|
||
|
||
if (startDate.HasValue && commitDate < startDate.Value)
|
||
continue;
|
||
if (endDate.HasValue && commitDate > endDate.Value)
|
||
continue;
|
||
|
||
// Пропускаем merge коммиты
|
||
if (IsMergeCommit(commit))
|
||
continue;
|
||
|
||
var parentTree = commit.Parents.FirstOrDefault()?.Tree;
|
||
var treeChanges = repo.Diff.Compare<TreeChanges>(parentTree, commit.Tree);
|
||
|
||
var patch = repo.Diff.Compare<Patch>(parentTree, commit.Tree);
|
||
var additions = patch.Sum(p => p.LinesAdded);
|
||
var deletions = patch.Sum(p => p.LinesDeleted);
|
||
|
||
var commitInfo = new CommitInfo
|
||
{
|
||
Sha = commit.Sha,
|
||
Message = commit.MessageShort,
|
||
Author = commit.Author.Name,
|
||
AuthorEmail = commit.Author.Email,
|
||
Committer = commit.Committer.Name,
|
||
CommitterEmail = commit.Committer.Email,
|
||
When = commit.Author.When,
|
||
FilesChanged = treeChanges.Count,
|
||
Additions = additions,
|
||
Deletions = deletions,
|
||
ChangedPaths = treeChanges.Select(t => t.Path).ToList()
|
||
};
|
||
|
||
commits.Add(commitInfo);
|
||
}
|
||
|
||
return commits;
|
||
});
|
||
}
|
||
|
||
public async Task<SummaryResponse> GenerateSummaryAsync(string repositoryPath, DateTime? startDate, DateTime? endDate)
|
||
{
|
||
var commits = await GetCommitsAsync(repositoryPath, startDate, endDate);
|
||
|
||
if (!commits.Any())
|
||
{
|
||
return new SummaryResponse
|
||
{
|
||
Period = new PeriodInfo
|
||
{
|
||
StartDate = startDate ?? DateTime.MinValue,
|
||
EndDate = endDate ?? DateTime.MaxValue,
|
||
Description = "Период без коммитов"
|
||
}
|
||
};
|
||
}
|
||
|
||
var stats = new Statistics
|
||
{
|
||
TotalCommits = commits.Count,
|
||
TotalFilesChanged = commits.Sum(c => c.FilesChanged),
|
||
TotalAdditions = commits.Sum(c => c.Additions),
|
||
TotalDeletions = commits.Sum(c => c.Deletions),
|
||
UniqueContributors = commits.SelectMany(c => new[] { c.Author, c.Committer }).Distinct().Count()
|
||
};
|
||
|
||
var categories = CategorizeCommits(commits);
|
||
var topCommits = commits
|
||
.OrderByDescending(c => c.FilesChanged + c.Additions + c.Deletions)
|
||
.Take(10)
|
||
.ToList();
|
||
|
||
return new SummaryResponse
|
||
{
|
||
Period = new PeriodInfo
|
||
{
|
||
StartDate = startDate ?? commits.Min(c => c.When.DateTime),
|
||
EndDate = endDate ?? commits.Max(c => c.When.DateTime),
|
||
Description = $"С {startDate?.ToString("yyyy-MM-dd") ?? "начала"} по {endDate?.ToString("yyyy-MM-dd") ?? "конца"}"
|
||
},
|
||
Stats = stats,
|
||
Categories = categories,
|
||
TopCommits = topCommits
|
||
};
|
||
}
|
||
|
||
private List<CategorySummary> CategorizeCommits(List<CommitInfo> commits)
|
||
{
|
||
var categories = new Dictionary<string, CategorySummary>();
|
||
|
||
foreach (var commit in commits)
|
||
{
|
||
var category = DetermineCategory(commit);
|
||
|
||
if (!categories.ContainsKey(category))
|
||
{
|
||
categories[category] = new CategorySummary
|
||
{
|
||
Category = category,
|
||
Description = GetCategoryDescription(category),
|
||
CommitsCount = 0,
|
||
KeyChanges = new List<string>()
|
||
};
|
||
}
|
||
|
||
categories[category].CommitsCount++;
|
||
|
||
if (commit.FilesChanged > 5 || commit.Additions + commit.Deletions > 100)
|
||
{
|
||
categories[category].KeyChanges.Add(commit.Message);
|
||
}
|
||
}
|
||
|
||
return categories.Values
|
||
.OrderByDescending(c => c.CommitsCount)
|
||
.ToList();
|
||
}
|
||
|
||
private string DetermineCategory(CommitInfo commit)
|
||
{
|
||
var message = commit.Message.ToLower();
|
||
var paths = string.Join(" ", commit.ChangedPaths).ToLower();
|
||
|
||
// Определение категории по сообщению коммита и измененным путям
|
||
if (message.Contains("fix") || message.Contains("bug") || message.Contains("исправ"))
|
||
return "Исправления ошибок";
|
||
|
||
if (message.Contains("feat") || message.Contains("add") || message.Contains("новая") || message.Contains("добав"))
|
||
return "Новые функции";
|
||
|
||
if (message.Contains("refactor") || message.Contains("refactoring") || message.Contains("рефакторинг"))
|
||
return "Рефакторинг";
|
||
|
||
if (message.Contains("test") || message.Contains("тест") || paths.Contains("test"))
|
||
return "Тестирование";
|
||
|
||
if (paths.Contains("api") || paths.Contains("controller") || paths.Contains("endpoint"))
|
||
return "API изменения";
|
||
|
||
if (paths.Contains("ui") || paths.Contains("view") || paths.Contains("component") || paths.Contains("frontend"))
|
||
return "UI изменения";
|
||
|
||
if (paths.Contains("config") || paths.Contains("setting") || message.Contains("config"))
|
||
return "Конфигурация";
|
||
|
||
if (message.Contains("doc") || message.Contains("readme") || message.Contains("документ"))
|
||
return "Документация";
|
||
|
||
if (message.Contains("perf") || message.Contains("optimize") || message.Contains("производительность"))
|
||
return "Оптимизация производительности";
|
||
|
||
if (message.Contains("security") || message.Contains("безопасность"))
|
||
return "Безопасность";
|
||
|
||
return "Прочие изменения";
|
||
}
|
||
|
||
private string GetCategoryDescription(string category)
|
||
{
|
||
return category switch
|
||
{
|
||
"Исправления ошибок" => "Работа над исправлением багов и ошибок",
|
||
"Новые функции" => "Разработка и добавление новых возможностей",
|
||
"Рефакторинг" => "Улучшение структуры кода без изменения функциональности",
|
||
"Тестирование" => "Добавление и улучшение тестов",
|
||
"API изменения" => "Изменения в API и эндпоинтах",
|
||
"UI изменения" => "Изменения в пользовательском интерфейсе",
|
||
"Конфигурация" => "Изменения в настройках и конфигурации",
|
||
"Документация" => "Обновление документации",
|
||
"Оптимизация производительности" => "Улучшение производительности приложения",
|
||
"Безопасность" => "Улучшения безопасности",
|
||
_ => "Различные изменения в проекте"
|
||
};
|
||
}
|
||
|
||
private bool IsMergeCommit(Commit commit)
|
||
{
|
||
// Merge коммиты имеют больше одного родителя
|
||
if (commit.Parents.Count() > 1)
|
||
return true;
|
||
|
||
// Проверяем сообщение коммита на наличие типичных merge паттернов
|
||
var message = commit.MessageShort.ToLower();
|
||
var fullMessage = commit.Message.ToLower();
|
||
|
||
var mergePatterns = new[]
|
||
{
|
||
"merge branch",
|
||
"merge pull request",
|
||
"merge remote",
|
||
"merged ",
|
||
"merge from",
|
||
"merge into",
|
||
"merge:",
|
||
"merging",
|
||
"merge commit",
|
||
"слияние ветки",
|
||
"слияние веток",
|
||
"мерж ветки"
|
||
};
|
||
|
||
return mergePatterns.Any(pattern => message.Contains(pattern) || fullMessage.Contains(pattern));
|
||
}
|
||
}
|