using LibGit2Sharp; using PerfReviewSummarizer.Api.Models; namespace PerfReviewSummarizer.Api.Services; public class GitService : IGitService { public async Task> GetCommitsAsync(string repositoryPath, DateTime? startDate, DateTime? endDate) { return await Task.Run(() => { var commits = new List(); 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(parentTree, commit.Tree); var patch = repo.Diff.Compare(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 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 CategorizeCommits(List commits) { var categories = new Dictionary(); 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() }; } 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)); } }