Add IPC backoff/retries and safer disposal

Introduce exponential backoff, jitter and retry logic across IPC components to improve robustness and avoid tight retry loops; make disposal idempotent and add connection guards. Key changes:
- LauncherCoordinatorIpcServer / LauncherIpcServer: add backoff constants, ComputeBackoff(), consecutive error tracking and delayed retries with jitter.
- LanMountainDesktopIpcClient / LauncherIpcClient: add connect retry loops, timeouts, delayed retries, improved error logging, and use ArrayPool for buffered async writes; ensure proper cleanup on failures.
- PublicIpcHostService: add disposed flag, guard OnPeerConnected and Dispose, and clear connected peers on dispose.
- Add many auto-generated commit analysis docs under docs/auto_commit_md and new scripts for analyzing/generating commit docs.
These changes aim to make IPC connection handling more resilient and resource-safe.
This commit is contained in:
lincube
2026-05-07 21:39:21 +08:00
parent 84caca02bf
commit d8f75e86be
159 changed files with 8809 additions and 31 deletions

View File

@@ -0,0 +1,552 @@
<#
.SYNOPSIS
Git Commit 深度分析工具
用于解析 Git 对象文件并生成详细的代码变更分析报告
#>
param(
[string]$RepoPath = "d:\github\LanMountainDesktop",
[string]$OutputDir = "docs\auto_commit_md"
)
# 添加压缩支持
Add-Type -AssemblyName System.IO.Compression
function Read-GitObject {
param([string]$RepoPath, [string]$ObjHash)
if ($ObjHash.Length -lt 4) { return $null }
$objDir = $ObjHash.Substring(0, 2)
$objFile = $ObjHash.Substring(2)
$objPath = Join-Path $RepoPath ".git\objects\$objDir\$objFile"
if (-not (Test-Path $objPath)) { return $null }
try {
$compressedData = [System.IO.File]::ReadAllBytes($objPath)
# 使用 .NET 解压缩
$ms = New-Object System.IO.MemoryStream(,$compressedData)
$deflate = New-Object System.IO.Compression.DeflateStream($ms, [System.IO.Compression.CompressionMode]::Decompress)
$reader = New-Object System.IO.StreamReader($deflate)
$content = $reader.ReadToEnd()
$reader.Close()
$deflate.Close()
$ms.Close()
# 解析对象头
$nullIdx = $content.IndexOf("`0")
if ($nullIdx -eq -1) { return $null }
$header = $content.Substring(0, $nullIdx)
$body = $content.Substring($nullIdx + 1)
$objType = $header.Split(' ')[0]
return @{
Type = $objType
Content = $body
RawContent = [System.Text.Encoding]::UTF8.GetBytes($body)
}
}
catch {
Write-Host "Error reading object ${ObjHash}: $_" -ForegroundColor Red
return $null
}
}
function Parse-Commit {
param([string]$RepoPath, [string]$CommitHash)
$obj = Read-GitObject -RepoPath $RepoPath -ObjHash $CommitHash
if (-not $obj -or $obj.Type -ne 'commit') { return $null }
$content = $obj.Content
$lines = $content -split "`n"
$parent = $null
$tree = $null
$author = $null
$email = $null
$timestamp = $null
$timezone = $null
$messageLines = @()
$inMessage = $false
foreach ($line in $lines) {
if ($inMessage) {
$messageLines += $line
}
elseif ($line -match '^tree (.+)') {
$tree = $matches[1].Trim()
}
elseif ($line -match '^parent (.+)') {
$parent = $matches[1].Trim()
}
elseif ($line -match '^author (.+) <(.+)> (\d+) ([+-]\d+)') {
$author = $matches[1]
$email = $matches[2]
$timestamp = [int]$matches[3]
$timezone = $matches[4]
}
elseif ($line -eq '') {
$inMessage = $true
}
}
$message = ($messageLines -join "`n").Trim()
return @{
Hash = $CommitHash
Parent = $parent
Tree = $tree
Author = $author
Email = $email
Timestamp = $timestamp
Timezone = $timezone
Message = $message
}
}
function Parse-Tree {
param([string]$RepoPath, [string]$TreeHash)
$obj = Read-GitObject -RepoPath $RepoPath -ObjHash $TreeHash
if (-not $obj -or $obj.Type -ne 'tree') { return @{} }
$entries = @{}
$content = $obj.RawContent
$idx = 0
while ($idx -lt $content.Length) {
# 查找空格
$spaceIdx = [Array]::IndexOf($content, [byte][char]' ', $idx)
if ($spaceIdx -eq -1) { break }
$mode = [System.Text.Encoding]::UTF8.GetString($content[$idx..($spaceIdx-1)])
# 查找 null
$nullIdx = [Array]::IndexOf($content, [byte]0, $spaceIdx)
if ($nullIdx -eq -1) { break }
$name = [System.Text.Encoding]::UTF8.GetString($content[($spaceIdx+1)..($nullIdx-1)])
# 读取 20 字节 SHA
$shaStart = $nullIdx + 1
$shaEnd = $shaStart + 20
if ($shaEnd -gt $content.Length) { break }
$shaBytes = $content[$shaStart..($shaEnd-1)]
$sha = [BitConverter]::ToString($shaBytes).Replace("-", "").ToLower()
$entries[$name] = $sha
$idx = $shaEnd
}
return $entries
}
function Get-CommitChanges {
param([string]$RepoPath, [string]$CommitHash)
$commit = Parse-Commit -RepoPath $RepoPath -CommitHash $CommitHash
if (-not $commit) { return @() }
$currentTree = Parse-Tree -RepoPath $RepoPath -TreeHash $commit.Tree
$parentTree = @{}
if ($commit.Parent) {
$parentCommit = Parse-Commit -RepoPath $RepoPath -CommitHash $commit.Parent
if ($parentCommit) {
$parentTree = Parse-Tree -RepoPath $RepoPath -TreeHash $parentCommit.Tree
}
}
$changes = @()
$stats = @{ Added = 0; Modified = 0; Deleted = 0 }
$allPaths = ($currentTree.Keys + $parentTree.Keys) | Select-Object -Unique
foreach ($path in $allPaths) {
if ($currentTree.ContainsKey($path) -and -not $parentTree.ContainsKey($path)) {
$changes += @{ Path = $path; Type = 'added' }
$stats.Added++
}
elseif (-not $currentTree.ContainsKey($path) -and $parentTree.ContainsKey($path)) {
$changes += @{ Path = $path; Type = 'deleted' }
$stats.Deleted++
}
elseif ($currentTree[$path] -ne $parentTree[$path]) {
$changes += @{ Path = $path; Type = 'modified' }
$stats.Modified++
}
}
return @{
Changes = $changes
Stats = $stats
Commit = $commit
}
}
function Assess-Importance {
param([string]$Message, [array]$Changes, [hashtable]$Stats)
$msgLower = $Message.ToLower()
$criticalKeywords = @('fix', 'bug', 'security', 'crash', 'memory leak', 'deadlock')
$featureKeywords = @('feat', 'feature', 'add', 'implement', 'new')
$refactorKeywords = @('refactor', 'restructure', 'cleanup', 'optimize')
foreach ($kw in $criticalKeywords) {
if ($msgLower -like "*$kw*") { return 'critical' }
}
foreach ($kw in $featureKeywords) {
if ($msgLower -like "*$kw*") { return 'feature' }
}
$totalChanges = $Stats.Added + $Stats.Modified + $Stats.Deleted
if ($totalChanges -gt 20) { return 'major' }
foreach ($kw in $refactorKeywords) {
if ($msgLower -like "*$kw*") { return 'refactor' }
}
return 'minor'
}
function Get-FileTypeDistribution {
param([array]$Changes)
$fileTypes = @{}
foreach ($change in $Changes) {
$ext = [System.IO.Path]::GetExtension($change.Path)
if ([string]::IsNullOrEmpty($ext)) { $ext = 'no_extension' }
if (-not $fileTypes.ContainsKey($ext)) { $fileTypes[$ext] = 0 }
$fileTypes[$ext]++
}
return $fileTypes
}
function Analyze-Impact {
param([array]$Changes, [string]$Message)
$impacts = @()
# 分析受影响的模块
$modules = @{}
foreach ($change in $Changes) {
$parts = $change.Path -split '/'
if ($parts.Length -gt 1) {
if (-not $modules.ContainsKey($parts[0])) { $modules[$parts[0]] = 0 }
$modules[$parts[0]]++
}
}
if ($modules.Count -gt 0) {
$moduleList = ($modules.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 5 | ForEach-Object { $_.Key }) -join ', '
$impacts += "受影响的模块: $moduleList"
}
# 分析文件类型
$fileTypes = Get-FileTypeDistribution -Changes $Changes
if ($fileTypes.ContainsKey('.cs')) {
$impacts += "涉及 $($fileTypes['.cs']) 个 C# 文件变更"
}
if ($fileTypes.ContainsKey('.axaml') -or $fileTypes.ContainsKey('.xaml')) {
$impacts += "涉及 UI/XAML 文件变更"
}
if ($fileTypes.ContainsKey('.md')) {
$impacts += "涉及文档更新"
}
# 根据提交消息分析
$msgLower = $Message.ToLower()
if ($msgLower -like '*fix*') {
$impacts += "这是一个修复性提交,可能解决现有问题"
}
if ($msgLower -like '*feat*' -or $msgLower -like '*feature*') {
$impacts += "这是一个功能新增提交,扩展了项目能力"
}
if ($msgLower -like '*refactor*') {
$impacts += "这是一个重构提交,改善了代码结构"
}
return $impacts
}
function Generate-ReviewPoints {
param([array]$Changes, [string]$Message)
$points = @()
# 检查关键文件
$criticalPatterns = @('Program.cs', 'App.axaml', 'MainWindow', 'Core', 'Service')
foreach ($change in $Changes) {
foreach ($pattern in $criticalPatterns) {
if ($change.Path -like "*$pattern*") {
$points += "关键文件变更: $($change.Path) - 需要特别关注"
break
}
}
}
# 检查提交消息质量
if ($Message.Length -lt 10) {
$points += "提交消息较短,建议提供更详细的变更说明"
}
if ($Message.ToLower() -like '*wip*' -or $Message.ToLower() -like '*todo*') {
$points += "提交包含 WIP/TODO 标记,确认是否已完成"
}
# 检查文件删除
$deleted = $Changes | Where-Object { $_.Type -eq 'deleted' }
if ($deleted.Count -gt 0) {
$points += "删除了 $($deleted.Count) 个文件,确认是否有其他代码依赖这些文件"
}
return $points
}
function Get-KeySnippets {
param([string]$RepoPath, [array]$Changes)
$snippets = @()
$count = 0
foreach ($change in $Changes | Select-Object -First 10) {
if ($change.Type -eq 'deleted') { continue }
$filePath = Join-Path $RepoPath $change.Path
if (Test-Path $filePath -PathType Leaf) {
try {
$content = Get-Content $filePath -Raw -Encoding UTF8 -ErrorAction SilentlyContinue
if ($content) {
$lines = $content -split "`n"
$preview = if ($lines.Count -gt 30) { ($lines[0..29] -join "`n") } else { $content }
$snippets += @{
File = $change.Path
Type = $change.Type
LinesCount = $lines.Count
Preview = $preview.Substring(0, [Math]::Min(2000, $preview.Length))
}
$count++
}
}
catch {
# 忽略无法读取的文件
}
}
}
return $snippets
}
function Generate-MarkdownReport {
param([hashtable]$Analysis)
$lines = @()
# 标题
$lines += "# Commit 深度分析报告"
$lines += ""
$lines += "**提交哈希**: ``$($Analysis.CommitHash)``"
$lines += "**提交时间**: $($Analysis.Date)"
$lines += "**作者**: $($Analysis.Author) <$($Analysis.Email)>"
$lines += "**重要性**: $($Analysis.Importance.ToUpper())"
$lines += ""
# 提交消息
$lines += "## 提交消息"
$lines += "``````"
$lines += $Analysis.Message
$lines += "``````"
$lines += ""
# 变更统计
$lines += "## 变更统计"
$lines += "- **新增文件**: $($Analysis.Stats.Added)"
$lines += "- **修改文件**: $($Analysis.Stats.Modified)"
$lines += "- **删除文件**: $($Analysis.Stats.Deleted)"
$lines += ""
# 文件类型分布
if ($Analysis.FileTypes.Count -gt 0) {
$lines += "### 文件类型分布"
$sortedTypes = $Analysis.FileTypes.GetEnumerator() | Sort-Object Value -Descending
foreach ($ft in $sortedTypes) {
$lines += "- ``$($ft.Key)``: $($ft.Value) 个文件"
}
$lines += ""
}
# 变更文件列表
if ($Analysis.Changes.Count -gt 0) {
$lines += "## 变更文件列表"
$lines += "| 文件路径 | 变更类型 |"
$lines += "|---------|---------|"
$typeMap = @{ 'added' = '新增'; 'modified' = '修改'; 'deleted' = '删除' }
foreach ($change in $Analysis.Changes | Select-Object -First 50) {
$typeStr = if ($typeMap.ContainsKey($change.Type)) { $typeMap[$change.Type] } else { $change.Type }
$lines += "| ``$($change.Path)`` | $typeStr |"
}
$lines += ""
}
# 影响分析
if ($Analysis.ImpactAnalysis.Count -gt 0) {
$lines += "## 影响分析"
foreach ($impact in $Analysis.ImpactAnalysis) {
$lines += "- $impact"
}
$lines += ""
}
# 代码审查要点
if ($Analysis.ReviewPoints.Count -gt 0) {
$lines += "## 代码审查要点"
foreach ($point in $Analysis.ReviewPoints) {
$lines += "- ⚠️ $point"
}
$lines += ""
}
# 关键代码片段
if ($Analysis.KeySnippets.Count -gt 0) {
$lines += "## 关键代码片段"
foreach ($snippet in $Analysis.KeySnippets | Select-Object -First 5) {
$lines += "### $($snippet.File)"
$lines += "- **类型**: $($snippet.Type)"
$lines += "- **行数**: $($snippet.LinesCount)"
$lines += ""
$lines += "``````"
$lines += $snippet.Preview
$lines += "``````"
$lines += ""
}
}
return $lines -join "`n"
}
# 主逻辑
Write-Host "Git Commit 深度分析工具" -ForegroundColor Cyan
Write-Host "======================" -ForegroundColor Cyan
Write-Host ""
# 确保输出目录存在
$outputPath = Join-Path $RepoPath $OutputDir
if (-not (Test-Path $outputPath)) {
New-Item -ItemType Directory -Path $outputPath -Force | Out-Null
}
# 读取 HEAD 日志
$headLogPath = Join-Path $RepoPath ".git\logs\HEAD"
if (-not (Test-Path $headLogPath)) {
Write-Host "错误: 找不到 HEAD 日志文件: $headLogPath" -ForegroundColor Red
exit 1
}
# 解析 HEAD 日志
$commits = @()
$logContent = Get-Content $headLogPath
foreach ($line in $logContent) {
$line = $line.Trim()
if ([string]::IsNullOrEmpty($line)) { continue }
# 解析日志行
$parts = $line -split "`t"
if ($parts.Count -lt 2) { continue }
$metaPart = $parts[0]
$actionPart = $parts[1]
$metaTokens = $metaPart -split '\s+'
if ($metaTokens.Count -lt 5) { continue }
$newHash = $metaTokens[1]
# 只处理 commit 操作
if ($actionPart -match 'commit' -or $actionPart -match '^commit:') {
$message = $actionPart -replace '^commit:\s*', ''
$commits += @{
Hash = $newHash
Message = $message
}
}
}
Write-Host "找到 $($commits.Count) 个 commit" -ForegroundColor Green
Write-Host ""
# 分析每个 commit
$processed = 0
$success = 0
foreach ($commitInfo in $commits) {
$commitHash = $commitInfo.Hash
$shortHash = $commitHash.Substring(0, 7)
$processed++
Write-Host "[$processed/$($commits.Count)] 分析 commit: $shortHash - $($commitInfo.Message.Substring(0, [Math]::Min(50, $commitInfo.Message.Length)))" -NoNewline
try {
# 获取变更
$changeInfo = Get-CommitChanges -RepoPath $RepoPath -CommitHash $commitHash
if (-not $changeInfo) {
Write-Host " [跳过]" -ForegroundColor Yellow
continue
}
$commit = $changeInfo.Commit
$changes = $changeInfo.Changes
$stats = $changeInfo.Stats
# 分析
$importance = Assess-Importance -Message $commit.Message -Changes $changes -Stats $stats
$fileTypes = Get-FileTypeDistribution -Changes $changes
$impactAnalysis = Analyze-Impact -Changes $changes -Message $commit.Message
$reviewPoints = Generate-ReviewPoints -Changes $changes -Message $commit.Message
$keySnippets = Get-KeySnippets -RepoPath $RepoPath -Changes $changes
# 构建分析结果
$analysis = @{
CommitHash = $commitHash
Message = $commit.Message
Author = $commit.Author
Email = $commit.Email
Timestamp = $commit.Timestamp
Date = (Get-Date -Date ([DateTime]::UnixEpoch.AddSeconds($commit.Timestamp)) -Format 'yyyy-MM-dd HH:mm:ss')
Stats = $stats
Changes = $changes
FileTypes = $fileTypes
Importance = $importance
ImpactAnalysis = $impactAnalysis
ReviewPoints = $reviewPoints
KeySnippets = $keySnippets
}
# 生成报告
$report = Generate-MarkdownReport -Analysis $analysis
# 保存报告
$dateStr = Get-Date -Date ([DateTime]::UnixEpoch.AddSeconds($commit.Timestamp)) -Format 'yyyyMMdd'
$filename = "${dateStr}_${shortHash}_deep_analysis.md"
$outputFile = Join-Path $outputPath $filename
$report | Out-File -FilePath $outputFile -Encoding UTF8
Write-Host " [已保存]" -ForegroundColor Green
$success++
}
catch {
Write-Host " [错误: $_]" -ForegroundColor Red
}
}
Write-Host ""
Write-Host "分析完成! 成功处理 $success / $processed 个 commit" -ForegroundColor Cyan

View File

@@ -0,0 +1,662 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace GitCommitAnalyzer
{
public class GitObject
{
public string Type { get; set; }
public byte[] Content { get; set; }
}
public class CommitInfo
{
public string Hash { get; set; }
public string Parent { get; set; }
public string Tree { get; set; }
public string Author { get; set; }
public string Email { get; set; }
public long Timestamp { get; set; }
public string Timezone { get; set; }
public string Message { get; set; }
}
public class FileChange
{
public string Path { get; set; }
public string ChangeType { get; set; }
}
public class CommitAnalysis
{
public string CommitHash { get; set; }
public string Message { get; set; }
public string Author { get; set; }
public string Email { get; set; }
public long Timestamp { get; set; }
public string Date { get; set; }
public Dictionary<string, int> Stats { get; set; }
public List<FileChange> Changes { get; set; }
public Dictionary<string, int> FileTypes { get; set; }
public string Importance { get; set; }
public List<string> ImpactAnalysis { get; set; }
public List<string> ReviewPoints { get; set; }
public List<KeySnippet> KeySnippets { get; set; }
}
public class KeySnippet
{
public string File { get; set; }
public string Type { get; set; }
public int LinesCount { get; set; }
public string Preview { get; set; }
}
public class GitObjectParser
{
private readonly string _repoPath;
private readonly string _objectsPath;
private readonly Dictionary<string, CommitInfo> _commitCache = new();
private readonly Dictionary<string, Dictionary<string, string>> _treeCache = new();
public GitObjectParser(string repoPath)
{
_repoPath = repoPath;
_objectsPath = Path.Combine(repoPath, ".git", "objects");
}
public GitObject ReadObject(string objHash)
{
if (objHash.Length < 4) return null;
var objDir = objHash.Substring(0, 2);
var objFile = objHash.Substring(2);
var objPath = Path.Combine(_objectsPath, objDir, objFile);
if (!File.Exists(objPath)) return null;
try
{
var compressedData = File.ReadAllBytes(objPath);
// 使用 zlib 解压缩
using var ms = new MemoryStream(compressedData);
// 跳过 zlib 头 (2 字节)
ms.ReadByte();
ms.ReadByte();
using var deflate = new DeflateStream(ms, CompressionMode.Decompress);
using var result = new MemoryStream();
deflate.CopyTo(result);
var decompressed = result.ToArray();
// 解析对象头
var nullIdx = Array.IndexOf(decompressed, (byte)0);
if (nullIdx == -1) return null;
var header = Encoding.UTF8.GetString(decompressed, 0, nullIdx);
var objType = header.Split(' ')[0];
var content = new byte[decompressed.Length - nullIdx - 1];
Array.Copy(decompressed, nullIdx + 1, content, 0, content.Length);
return new GitObject { Type = objType, Content = content };
}
catch (Exception ex)
{
Console.WriteLine($"Error reading object {objHash}: {ex.Message}");
return null;
}
}
public CommitInfo ParseCommit(string commitHash)
{
if (_commitCache.ContainsKey(commitHash))
return _commitCache[commitHash];
var obj = ReadObject(commitHash);
if (obj == null || obj.Type != "commit")
return null;
var content = Encoding.UTF8.GetString(obj.Content);
var lines = content.Split('\n');
string parent = null, tree = null, author = null, email = null, timezone = null;
long timestamp = 0;
var messageLines = new List<string>();
var inMessage = false;
foreach (var line in lines)
{
if (inMessage)
{
messageLines.Add(line);
}
else if (line.StartsWith("tree "))
{
tree = line.Substring(5).Trim();
}
else if (line.StartsWith("parent "))
{
parent = line.Substring(7).Trim();
}
else if (line.StartsWith("author "))
{
var match = Regex.Match(line, @"^author (.+) <(.+)> (\d+) ([+-]\d+)$");
if (match.Success)
{
author = match.Groups[1].Value;
email = match.Groups[2].Value;
timestamp = long.Parse(match.Groups[3].Value);
timezone = match.Groups[4].Value;
}
}
else if (line == "")
{
inMessage = true;
}
}
var message = string.Join("\n", messageLines).Trim();
var commitInfo = new CommitInfo
{
Hash = commitHash,
Parent = parent,
Tree = tree,
Author = author ?? "Unknown",
Email = email ?? "",
Timestamp = timestamp,
Timezone = timezone ?? "",
Message = message
};
_commitCache[commitHash] = commitInfo;
return commitInfo;
}
public Dictionary<string, string> ParseTree(string treeHash)
{
if (_treeCache.ContainsKey(treeHash))
return _treeCache[treeHash];
var obj = ReadObject(treeHash);
if (obj == null || obj.Type != "tree")
return new Dictionary<string, string>();
var entries = new Dictionary<string, string>();
var content = obj.Content;
var idx = 0;
while (idx < content.Length)
{
// 查找空格
var spaceIdx = Array.IndexOf(content, (byte)' ', idx);
if (spaceIdx == -1) break;
var mode = Encoding.UTF8.GetString(content, idx, spaceIdx - idx);
// 查找 null
var nullIdx = Array.IndexOf(content, (byte)0, spaceIdx);
if (nullIdx == -1) break;
var name = Encoding.UTF8.GetString(content, spaceIdx + 1, nullIdx - spaceIdx - 1);
// 读取 20 字节 SHA
var shaStart = nullIdx + 1;
var shaEnd = shaStart + 20;
if (shaEnd > content.Length) break;
var shaBytes = new byte[20];
Array.Copy(content, shaStart, shaBytes, 0, 20);
var sha = BitConverter.ToString(shaBytes).Replace("-", "").ToLower();
entries[name] = sha;
idx = shaEnd;
}
_treeCache[treeHash] = entries;
return entries;
}
public (List<FileChange> Changes, Dictionary<string, int> Stats, CommitInfo Commit) GetCommitChanges(string commitHash)
{
var commit = ParseCommit(commitHash);
if (commit == null)
return (new List<FileChange>(), new Dictionary<string, int>(), null);
var currentTree = ParseTree(commit.Tree);
var parentTree = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(commit.Parent))
{
var parentCommit = ParseCommit(commit.Parent);
if (parentCommit != null)
{
parentTree = ParseTree(parentCommit.Tree);
}
}
var changes = new List<FileChange>();
var stats = new Dictionary<string, int> { ["Added"] = 0, ["Modified"] = 0, ["Deleted"] = 0 };
var allPaths = currentTree.Keys.Union(parentTree.Keys).Distinct();
foreach (var path in allPaths)
{
if (currentTree.ContainsKey(path) && !parentTree.ContainsKey(path))
{
changes.Add(new FileChange { Path = path, ChangeType = "added" });
stats["Added"]++;
}
else if (!currentTree.ContainsKey(path) && parentTree.ContainsKey(path))
{
changes.Add(new FileChange { Path = path, ChangeType = "deleted" });
stats["Deleted"]++;
}
else if (currentTree.GetValueOrDefault(path) != parentTree.GetValueOrDefault(path))
{
changes.Add(new FileChange { Path = path, ChangeType = "modified" });
stats["Modified"]++;
}
}
return (changes, stats, commit);
}
}
public class CommitAnalyzer
{
private readonly GitObjectParser _parser;
private readonly string _repoPath;
public CommitAnalyzer(string repoPath)
{
_parser = new GitObjectParser(repoPath);
_repoPath = repoPath;
}
public CommitAnalysis AnalyzeCommit(string commitHash)
{
var (changes, stats, commit) = _parser.GetCommitChanges(commitHash);
if (commit == null)
return null;
var fileTypes = GetFileTypeDistribution(changes);
var importance = AssessImportance(commit.Message, changes, stats);
var impactAnalysis = AnalyzeImpact(changes, commit.Message);
var reviewPoints = GenerateReviewPoints(changes, commit.Message);
var keySnippets = GetKeySnippets(changes);
return new CommitAnalysis
{
CommitHash = commitHash,
Message = commit.Message,
Author = commit.Author,
Email = commit.Email,
Timestamp = commit.Timestamp,
Date = DateTimeOffset.FromUnixTimeSeconds(commit.Timestamp).ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss"),
Stats = stats,
Changes = changes,
FileTypes = fileTypes,
Importance = importance,
ImpactAnalysis = impactAnalysis,
ReviewPoints = reviewPoints,
KeySnippets = keySnippets
};
}
private Dictionary<string, int> GetFileTypeDistribution(List<FileChange> changes)
{
var fileTypes = new Dictionary<string, int>();
foreach (var change in changes)
{
var ext = Path.GetExtension(change.Path);
if (string.IsNullOrEmpty(ext)) ext = "no_extension";
if (!fileTypes.ContainsKey(ext)) fileTypes[ext] = 0;
fileTypes[ext]++;
}
return fileTypes;
}
private string AssessImportance(string message, List<FileChange> changes, Dictionary<string, int> stats)
{
var msgLower = message.ToLower();
var criticalKeywords = new[] { "fix", "bug", "security", "crash", "memory leak", "deadlock" };
var featureKeywords = new[] { "feat", "feature", "add", "implement", "new" };
var refactorKeywords = new[] { "refactor", "restructure", "cleanup", "optimize" };
if (criticalKeywords.Any(kw => msgLower.Contains(kw))) return "critical";
if (featureKeywords.Any(kw => msgLower.Contains(kw))) return "feature";
var totalChanges = stats["Added"] + stats["Modified"] + stats["Deleted"];
if (totalChanges > 20) return "major";
if (refactorKeywords.Any(kw => msgLower.Contains(kw))) return "refactor";
return "minor";
}
private List<string> AnalyzeImpact(List<FileChange> changes, string message)
{
var impacts = new List<string>();
// 分析受影响的模块
var modules = new Dictionary<string, int>();
foreach (var change in changes)
{
var parts = change.Path.Split('/');
if (parts.Length > 1)
{
if (!modules.ContainsKey(parts[0])) modules[parts[0]] = 0;
modules[parts[0]]++;
}
}
if (modules.Count > 0)
{
var moduleList = string.Join(", ", modules.OrderByDescending(m => m.Value).Take(5).Select(m => m.Key));
impacts.Add($"受影响的模块: {moduleList}");
}
// 分析文件类型
var fileTypes = GetFileTypeDistribution(changes);
if (fileTypes.ContainsKey(".cs"))
impacts.Add($"涉及 {fileTypes[".cs"]} 个 C# 文件变更");
if (fileTypes.ContainsKey(".axaml") || fileTypes.ContainsKey(".xaml"))
impacts.Add("涉及 UI/XAML 文件变更");
if (fileTypes.ContainsKey(".md"))
impacts.Add("涉及文档更新");
// 根据提交消息分析
var msgLower = message.ToLower();
if (msgLower.Contains("fix"))
impacts.Add("这是一个修复性提交,可能解决现有问题");
if (msgLower.Contains("feat") || msgLower.Contains("feature"))
impacts.Add("这是一个功能新增提交,扩展了项目能力");
if (msgLower.Contains("refactor"))
impacts.Add("这是一个重构提交,改善了代码结构");
return impacts;
}
private List<string> GenerateReviewPoints(List<FileChange> changes, string message)
{
var points = new List<string>();
// 检查关键文件
var criticalPatterns = new[] { "Program.cs", "App.axaml", "MainWindow", "Core", "Service" };
foreach (var change in changes)
{
foreach (var pattern in criticalPatterns)
{
if (change.Path.Contains(pattern))
{
points.Add($"关键文件变更: {change.Path} - 需要特别关注");
break;
}
}
}
// 检查提交消息质量
if (message.Length < 10)
points.Add("提交消息较短,建议提供更详细的变更说明");
if (message.ToLower().Contains("wip") || message.ToLower().Contains("todo"))
points.Add("提交包含 WIP/TODO 标记,确认是否已完成");
// 检查文件删除
var deleted = changes.Where(c => c.ChangeType == "deleted").ToList();
if (deleted.Count > 0)
points.Add($"删除了 {deleted.Count} 个文件,确认是否有其他代码依赖这些文件");
return points;
}
private List<KeySnippet> GetKeySnippets(List<FileChange> changes)
{
var snippets = new List<KeySnippet>();
foreach (var change in changes.Take(10))
{
if (change.ChangeType == "deleted") continue;
var filePath = Path.Combine(_repoPath, change.Path);
if (File.Exists(filePath))
{
try
{
var content = File.ReadAllText(filePath, Encoding.UTF8);
var lines = content.Split('\n');
var preview = lines.Length > 30 ? string.Join("\n", lines.Take(30)) : content;
snippets.Add(new KeySnippet
{
File = change.Path,
Type = change.ChangeType,
LinesCount = lines.Length,
Preview = preview.Length > 2000 ? preview.Substring(0, 2000) : preview
});
}
catch
{
// 忽略无法读取的文件
}
}
}
return snippets;
}
}
public class ReportGenerator
{
public static string GenerateMarkdownReport(CommitAnalysis analysis)
{
var sb = new StringBuilder();
// 标题
sb.AppendLine("# Commit 深度分析报告");
sb.AppendLine();
sb.AppendLine($"**提交哈希**: `{analysis.CommitHash}`");
sb.AppendLine($"**提交时间**: {analysis.Date}");
sb.AppendLine($"**作者**: {analysis.Author} <{analysis.Email}>");
sb.AppendLine($"**重要性**: {analysis.Importance.ToUpper()}");
sb.AppendLine();
// 提交消息
sb.AppendLine("## 提交消息");
sb.AppendLine("```");
sb.AppendLine(analysis.Message);
sb.AppendLine("```");
sb.AppendLine();
// 变更统计
sb.AppendLine("## 变更统计");
sb.AppendLine($"- **新增文件**: {analysis.Stats["Added"]}");
sb.AppendLine($"- **修改文件**: {analysis.Stats["Modified"]}");
sb.AppendLine($"- **删除文件**: {analysis.Stats["Deleted"]}");
sb.AppendLine();
// 文件类型分布
if (analysis.FileTypes.Count > 0)
{
sb.AppendLine("### 文件类型分布");
foreach (var ft in analysis.FileTypes.OrderByDescending(x => x.Value))
{
sb.AppendLine($"- `{ft.Key}`: {ft.Value} 个文件");
}
sb.AppendLine();
}
// 变更文件列表
if (analysis.Changes.Count > 0)
{
sb.AppendLine("## 变更文件列表");
sb.AppendLine("| 文件路径 | 变更类型 |");
sb.AppendLine("|---------|---------|");
var typeMap = new Dictionary<string, string>
{
["added"] = "新增",
["modified"] = "修改",
["deleted"] = "删除"
};
foreach (var change in analysis.Changes.Take(50))
{
var typeStr = typeMap.GetValueOrDefault(change.ChangeType, change.ChangeType);
sb.AppendLine($"| `{change.Path}` | {typeStr} |");
}
sb.AppendLine();
}
// 影响分析
if (analysis.ImpactAnalysis.Count > 0)
{
sb.AppendLine("## 影响分析");
foreach (var impact in analysis.ImpactAnalysis)
{
sb.AppendLine($"- {impact}");
}
sb.AppendLine();
}
// 代码审查要点
if (analysis.ReviewPoints.Count > 0)
{
sb.AppendLine("## 代码审查要点");
foreach (var point in analysis.ReviewPoints)
{
sb.AppendLine($"- ⚠️ {point}");
}
sb.AppendLine();
}
// 关键代码片段
if (analysis.KeySnippets.Count > 0)
{
sb.AppendLine("## 关键代码片段");
foreach (var snippet in analysis.KeySnippets.Take(5))
{
sb.AppendLine($"### {snippet.File}");
sb.AppendLine($"- **类型**: {snippet.Type}");
sb.AppendLine($"- **行数**: {snippet.LinesCount}");
sb.AppendLine();
sb.AppendLine("```");
sb.AppendLine(snippet.Preview);
sb.AppendLine("```");
sb.AppendLine();
}
}
return sb.ToString();
}
}
class Program
{
static void Main(string[] args)
{
var repoPath = @"d:\github\LanMountainDesktop";
var outputDir = Path.Combine(repoPath, "docs", "auto_commit_md");
Console.WriteLine("Git Commit 深度分析工具");
Console.WriteLine("======================");
Console.WriteLine();
// 确保输出目录存在
Directory.CreateDirectory(outputDir);
// 读取 HEAD 日志
var headLogPath = Path.Combine(repoPath, ".git", "logs", "HEAD");
if (!File.Exists(headLogPath))
{
Console.WriteLine($"错误: 找不到 HEAD 日志文件: {headLogPath}");
return;
}
// 解析 HEAD 日志
var commits = new List<(string Hash, string Message)>();
var logLines = File.ReadAllLines(headLogPath);
foreach (var line in logLines)
{
var trimmedLine = line.Trim();
if (string.IsNullOrEmpty(trimmedLine)) continue;
var parts = trimmedLine.Split('\t');
if (parts.Length < 2) continue;
var metaPart = parts[0];
var actionPart = parts[1];
var metaTokens = metaPart.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (metaTokens.Length < 5) continue;
var newHash = metaTokens[1];
// 只处理 commit 操作
if (actionPart.Contains("commit"))
{
var message = actionPart.Replace("commit:", "").Trim();
commits.Add((newHash, message));
}
}
Console.WriteLine($"找到 {commits.Count} 个 commit");
Console.WriteLine();
// 初始化分析器
var analyzer = new CommitAnalyzer(repoPath);
// 分析每个 commit
var processed = 0;
var success = 0;
foreach (var commitInfo in commits)
{
var commitHash = commitInfo.Hash;
var shortHash = commitHash.Substring(0, 7);
processed++;
Console.Write($"[{processed}/{commits.Count}] 分析 commit: {shortHash} - {commitInfo.Message.Substring(0, Math.Min(50, commitInfo.Message.Length))}");
try
{
// 分析提交
var analysis = analyzer.AnalyzeCommit(commitHash);
if (analysis == null)
{
Console.WriteLine(" [跳过]");
continue;
}
// 生成报告
var report = ReportGenerator.GenerateMarkdownReport(analysis);
// 保存报告
var dateStr = DateTimeOffset.FromUnixTimeSeconds(analysis.Timestamp).ToLocalTime().ToString("yyyyMMdd");
var filename = $"{dateStr}_{shortHash}_deep_analysis.md";
var outputFile = Path.Combine(outputDir, filename);
File.WriteAllText(outputFile, report, Encoding.UTF8);
Console.WriteLine(" [已保存]");
success++;
}
catch (Exception ex)
{
Console.WriteLine($" [错误: {ex.Message}]");
}
}
Console.WriteLine();
Console.WriteLine($"分析完成! 成功处理 {success} / {processed} 个 commit");
}
}
}

View File

@@ -0,0 +1,600 @@
#!/usr/bin/env python3
"""
Git Commit 深度分析工具
用于解析 Git 对象文件并生成详细的代码变更分析报告
"""
import zlib
import os
import re
import json
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Tuple, Optional, Any
from dataclasses import dataclass, field
from collections import defaultdict
@dataclass
class GitObject:
"""Git 对象基类"""
obj_type: str
content: bytes
raw_data: bytes
@dataclass
class CommitInfo:
"""提交信息"""
hash: str
parent: Optional[str]
tree: str
author: str
email: str
timestamp: int
timezone: str
message: str
changes: List[Dict] = field(default_factory=list)
stats: Dict = field(default_factory=dict)
@dataclass
class FileChange:
"""文件变更信息"""
path: str
change_type: str # added, modified, deleted, renamed
old_path: Optional[str] = None
additions: int = 0
deletions: int = 0
diff_content: str = ""
class GitObjectParser:
"""Git 对象解析器"""
def __init__(self, repo_path: str):
self.repo_path = Path(repo_path)
self.objects_path = self.repo_path / ".git" / "objects"
self.commit_cache: Dict[str, CommitInfo] = {}
self.tree_cache: Dict[str, Dict[str, str]] = {}
def read_object(self, obj_hash: str) -> Optional[GitObject]:
"""读取并解压缩 Git 对象"""
if len(obj_hash) < 4:
return None
obj_dir = obj_hash[:2]
obj_file = obj_hash[2:]
obj_path = self.objects_path / obj_dir / obj_file
if not obj_path.exists():
return None
try:
with open(obj_path, 'rb') as f:
compressed_data = f.read()
# 解压缩 zlib
decompressed = zlib.decompress(compressed_data)
# 解析对象头和内容
null_idx = decompressed.index(b'\x00')
header = decompressed[:null_idx].decode('utf-8')
content = decompressed[null_idx + 1:]
obj_type = header.split()[0]
return GitObject(obj_type=obj_type, content=content, raw_data=decompressed)
except Exception as e:
print(f"Error reading object {obj_hash}: {e}")
return None
def parse_commit(self, commit_hash: str) -> Optional[CommitInfo]:
"""解析 commit 对象"""
if commit_hash in self.commit_cache:
return self.commit_cache[commit_hash]
obj = self.read_object(commit_hash)
if not obj or obj.obj_type != 'commit':
return None
try:
content = obj.content.decode('utf-8', errors='replace')
lines = content.split('\n')
parent = None
tree = None
author = None
email = None
timestamp = None
timezone = None
message_lines = []
in_message = False
for line in lines:
if in_message:
message_lines.append(line)
elif line.startswith('tree '):
tree = line[5:].strip()
elif line.startswith('parent '):
parent = line[7:].strip()
elif line.startswith('author '):
# author name <email> timestamp timezone
match = re.match(r'author (.+) <(.+)> (\d+) ([+-]\d+)', line)
if match:
author = match.group(1)
email = match.group(2)
timestamp = int(match.group(3))
timezone = match.group(4)
elif line == '':
in_message = True
message = '\n'.join(message_lines).strip()
commit_info = CommitInfo(
hash=commit_hash,
parent=parent,
tree=tree,
author=author or "Unknown",
email=email or "",
timestamp=timestamp or 0,
timezone=timezone or "",
message=message
)
self.commit_cache[commit_hash] = commit_info
return commit_info
except Exception as e:
print(f"Error parsing commit {commit_hash}: {e}")
return None
def parse_tree(self, tree_hash: str) -> Dict[str, str]:
"""解析 tree 对象,返回文件路径到 blob hash 的映射"""
if tree_hash in self.tree_cache:
return self.tree_cache[tree_hash]
obj = self.read_object(tree_hash)
if not obj or obj.obj_type != 'tree':
return {}
entries = {}
content = obj.content
idx = 0
while idx < len(content):
# 查找空格分隔符
space_idx = content.find(b' ', idx)
if space_idx == -1:
break
mode = content[idx:space_idx].decode('utf-8')
# 查找 null 分隔符
null_idx = content.find(b'\x00', space_idx)
if null_idx == -1:
break
name = content[space_idx + 1:null_idx].decode('utf-8', errors='replace')
# 读取 20 字节的 SHA
sha_start = null_idx + 1
sha_end = sha_start + 20
if sha_end > len(content):
break
sha = content[sha_start:sha_end].hex()
entries[name] = sha
idx = sha_end
self.tree_cache[tree_hash] = entries
return entries
def get_blob_content(self, blob_hash: str) -> Optional[str]:
"""获取 blob 对象的内容"""
obj = self.read_object(blob_hash)
if not obj or obj.obj_type != 'blob':
return None
try:
return obj.content.decode('utf-8', errors='replace')
except:
return None
def compare_trees(self, old_tree: str, new_tree: str) -> List[FileChange]:
"""比较两个 tree 对象,返回文件变更列表"""
old_files = self.parse_tree(old_tree) if old_tree else {}
new_files = self.parse_tree(new_tree) if new_tree else {}
changes = []
# 查找新增和修改的文件
for path, new_hash in new_files.items():
if path not in old_files:
changes.append(FileChange(path=path, change_type='added'))
elif old_files[path] != new_hash:
changes.append(FileChange(path=path, change_type='modified'))
# 查找删除的文件
for path in old_files:
if path not in new_files:
changes.append(FileChange(path=path, change_type='deleted'))
return changes
def get_commit_changes(self, commit_hash: str) -> Tuple[List[FileChange], Dict]:
"""获取提交的所有变更"""
commit = self.parse_commit(commit_hash)
if not commit:
return [], {}
# 获取当前提交的 tree
current_tree = self.parse_tree(commit.tree)
# 获取父提交的 tree
parent_tree = {}
if commit.parent:
parent_commit = self.parse_commit(commit.parent)
if parent_commit:
parent_tree = self.parse_tree(parent_commit.tree)
changes = []
stats = {'added': 0, 'modified': 0, 'deleted': 0, 'total_additions': 0, 'total_deletions': 0}
# 比较 tree
all_paths = set(current_tree.keys()) | set(parent_tree.keys())
for path in all_paths:
if path in current_tree and path not in parent_tree:
# 新增文件
changes.append(FileChange(path=path, change_type='added'))
stats['added'] += 1
elif path not in current_tree and path in parent_tree:
# 删除文件
changes.append(FileChange(path=path, change_type='deleted'))
stats['deleted'] += 1
elif current_tree.get(path) != parent_tree.get(path):
# 修改文件
changes.append(FileChange(path=path, change_type='modified'))
stats['modified'] += 1
return changes, stats
class CommitAnalyzer:
"""提交分析器"""
def __init__(self, repo_path: str):
self.parser = GitObjectParser(repo_path)
self.repo_path = Path(repo_path)
def analyze_commit(self, commit_hash: str) -> Dict[str, Any]:
"""分析单个提交"""
commit = self.parser.parse_commit(commit_hash)
if not commit:
return {}
changes, stats = self.parser.get_commit_changes(commit_hash)
# 分析文件类型
file_types = defaultdict(int)
for change in changes:
ext = Path(change.path).suffix or 'no_extension'
file_types[ext] += 1
# 分析变更的重要性
importance = self._assess_importance(commit.message, changes, stats)
# 提取关键代码片段
key_snippets = self._extract_key_snippets(changes)
return {
'commit_hash': commit_hash,
'message': commit.message,
'author': commit.author,
'email': commit.email,
'timestamp': commit.timestamp,
'date': datetime.fromtimestamp(commit.timestamp).strftime('%Y-%m-%d %H:%M:%S'),
'parent': commit.parent,
'changes': [
{
'path': c.path,
'type': c.change_type,
'additions': c.additions,
'deletions': c.deletions
}
for c in changes
],
'stats': stats,
'file_types': dict(file_types),
'importance': importance,
'key_snippets': key_snippets,
'impact_analysis': self._analyze_impact(changes, commit.message),
'review_points': self._generate_review_points(changes, commit.message)
}
def _assess_importance(self, message: str, changes: List[FileChange], stats: Dict) -> str:
"""评估提交的重要性"""
message_lower = message.lower()
# 检查关键关键词
critical_keywords = ['fix', 'bug', 'security', 'crash', 'memory leak', 'deadlock']
feature_keywords = ['feat', 'feature', 'add', 'implement', 'new']
refactor_keywords = ['refactor', 'restructure', 'cleanup', 'optimize']
if any(kw in message_lower for kw in critical_keywords):
return 'critical'
elif any(kw in message_lower for kw in feature_keywords):
return 'feature'
elif stats.get('added', 0) + stats.get('modified', 0) + stats.get('deleted', 0) > 20:
return 'major'
elif any(kw in message_lower for kw in refactor_keywords):
return 'refactor'
else:
return 'minor'
def _extract_key_snippets(self, changes: List[FileChange]) -> List[Dict]:
"""提取关键代码片段"""
snippets = []
for change in changes[:10]: # 限制分析的文件数量
if change.change_type == 'deleted':
continue
# 尝试读取文件内容
file_path = self.repo_path / change.path
if file_path.exists() and file_path.is_file():
try:
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
# 提取文件的基本信息
lines = content.split('\n')
snippet = {
'file': change.path,
'type': change.change_type,
'lines_count': len(lines),
'preview': '\n'.join(lines[:30]) if len(lines) > 30 else content
}
snippets.append(snippet)
except Exception:
pass
return snippets
def _analyze_impact(self, changes: List[FileChange], message: str) -> List[str]:
"""分析变更对项目的影响"""
impacts = []
# 分析受影响的模块
affected_modules = set()
for change in changes:
parts = change.path.split('/')
if len(parts) > 1:
affected_modules.add(parts[0])
if affected_modules:
impacts.append(f"受影响的模块: {', '.join(sorted(affected_modules))}")
# 分析文件类型影响
file_types = defaultdict(int)
for change in changes:
ext = Path(change.path).suffix
if ext:
file_types[ext] += 1
if '.cs' in file_types:
impacts.append(f"涉及 {file_types['.cs']} 个 C# 文件变更")
if '.axaml' in file_types or '.xaml' in file_types:
impacts.append("涉及 UI/XAML 文件变更")
if '.md' in file_types:
impacts.append("涉及文档更新")
# 根据提交消息分析
message_lower = message.lower()
if 'fix' in message_lower:
impacts.append("这是一个修复性提交,可能解决现有问题")
if 'feat' in message_lower or 'feature' in message_lower:
impacts.append("这是一个功能新增提交,扩展了项目能力")
if 'refactor' in message_lower:
impacts.append("这是一个重构提交,改善了代码结构")
if 'test' in message_lower:
impacts.append("涉及测试相关变更")
return impacts
def _generate_review_points(self, changes: List[FileChange], message: str) -> List[str]:
"""生成代码审查要点"""
points = []
# 检查大文件变更
large_files = [c for c in changes if c.additions + c.deletions > 100]
if large_files:
points.append(f"注意: 有 {len(large_files)} 个文件变更超过 100 行,需要仔细审查")
# 检查关键文件
critical_patterns = ['Program.cs', 'App.axaml', 'MainWindow', 'Core', 'Service']
for change in changes:
for pattern in critical_patterns:
if pattern in change.path:
points.append(f"关键文件变更: {change.path} - 需要特别关注")
break
# 检查提交消息质量
if len(message) < 10:
points.append("提交消息较短,建议提供更详细的变更说明")
if 'wip' in message.lower() or 'todo' in message.lower():
points.append("提交包含 WIP/TODO 标记,确认是否已完成")
# 检查文件删除
deleted = [c for c in changes if c.change_type == 'deleted']
if deleted:
points.append(f"删除了 {len(deleted)} 个文件,确认是否有其他代码依赖这些文件")
return points
def generate_markdown_report(analysis: Dict[str, Any]) -> str:
"""生成 Markdown 格式的分析报告"""
lines = []
# 标题
lines.append(f"# Commit 深度分析报告")
lines.append(f"")
lines.append(f"**提交哈希**: `{analysis['commit_hash']}`")
lines.append(f"**提交时间**: {analysis['date']}")
lines.append(f"**作者**: {analysis['author']} <{analysis['email']}>")
lines.append(f"**重要性**: {analysis['importance'].upper()}")
lines.append(f"")
# 提交消息
lines.append(f"## 提交消息")
lines.append(f"```")
lines.append(analysis['message'])
lines.append(f"```")
lines.append(f"")
# 变更统计
lines.append(f"## 变更统计")
stats = analysis['stats']
lines.append(f"- **新增文件**: {stats.get('added', 0)}")
lines.append(f"- **修改文件**: {stats.get('modified', 0)}")
lines.append(f"- **删除文件**: {stats.get('deleted', 0)}")
lines.append(f"")
# 文件类型分布
if analysis.get('file_types'):
lines.append(f"### 文件类型分布")
for ext, count in sorted(analysis['file_types'].items(), key=lambda x: -x[1]):
lines.append(f"- `{ext}`: {count} 个文件")
lines.append(f"")
# 变更文件列表
if analysis.get('changes'):
lines.append(f"## 变更文件列表")
lines.append(f"| 文件路径 | 变更类型 |")
lines.append(f"|---------|---------|")
type_map = {'added': '新增', 'modified': '修改', 'deleted': '删除'}
for change in analysis['changes'][:50]: # 限制显示数量
change_type = type_map.get(change['type'], change['type'])
lines.append(f"| `{change['path']}` | {change_type} |")
lines.append(f"")
# 影响分析
if analysis.get('impact_analysis'):
lines.append(f"## 影响分析")
for impact in analysis['impact_analysis']:
lines.append(f"- {impact}")
lines.append(f"")
# 代码审查要点
if analysis.get('review_points'):
lines.append(f"## 代码审查要点")
for point in analysis['review_points']:
lines.append(f"- ⚠️ {point}")
lines.append(f"")
# 关键代码片段
if analysis.get('key_snippets'):
lines.append(f"## 关键代码片段")
for snippet in analysis['key_snippets'][:5]:
lines.append(f"### {snippet['file']}")
lines.append(f"- **类型**: {snippet['type']}")
lines.append(f"- **行数**: {snippet['lines_count']}")
lines.append(f"")
lines.append(f"```")
lines.append(snippet['preview'][:2000]) # 限制预览长度
lines.append(f"```")
lines.append(f"")
return '\n'.join(lines)
def main():
"""主函数"""
repo_path = r"d:\github\LanMountainDesktop"
output_dir = Path(repo_path) / "docs" / "auto_commit_md"
# 确保输出目录存在
output_dir.mkdir(parents=True, exist_ok=True)
# 读取 HEAD 日志
head_log_path = Path(repo_path) / ".git" / "logs" / "HEAD"
if not head_log_path.exists():
print(f"错误: 找不到 HEAD 日志文件: {head_log_path}")
return
# 解析 HEAD 日志获取所有 commit
commits = []
with open(head_log_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
# 解析日志行
# 格式: old_hash new_hash name <email> timestamp timezone\taction: message
parts = line.split('\t')
if len(parts) < 2:
continue
meta_part = parts[0]
action_part = parts[1]
meta_tokens = meta_part.split()
if len(meta_tokens) < 5:
continue
new_hash = meta_tokens[1]
# 只处理 commit 操作
if 'commit' in action_part or action_part.startswith('commit:'):
message = action_part.replace('commit:', '').strip()
commits.append({
'hash': new_hash,
'message': message
})
print(f"找到 {len(commits)} 个 commit")
# 初始化分析器
analyzer = CommitAnalyzer(repo_path)
# 分析每个 commit
for i, commit_info in enumerate(commits):
commit_hash = commit_info['hash']
short_hash = commit_hash[:7]
print(f"[{i+1}/{len(commits)}] 分析 commit: {short_hash} - {commit_info['message'][:50]}")
try:
# 分析提交
analysis = analyzer.analyze_commit(commit_hash)
if not analysis:
print(f" 跳过: 无法解析 commit {short_hash}")
continue
# 生成报告
report = generate_markdown_report(analysis)
# 保存报告
date_str = datetime.fromtimestamp(analysis['timestamp']).strftime('%Y%m%d')
filename = f"{date_str}_{short_hash}_deep_analysis.md"
output_path = output_dir / filename
with open(output_path, 'w', encoding='utf-8') as f:
f.write(report)
print(f" 已保存: {filename}")
except Exception as e:
print(f" 错误: 分析 commit {short_hash} 时出错: {e}")
import traceback
traceback.print_exc()
print("\n分析完成!")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,389 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
解析 Git HEAD 日志文件并生成 Markdown 提交分析报告
"""
import re
import os
from datetime import datetime, timezone, timedelta
from pathlib import Path
def parse_head_log(log_content):
"""
解析 HEAD 日志内容,提取所有 commit 类型的提交
格式old_hash new_hash author_name <author_email> timestamp timezone\taction: message
"""
commits = []
# 匹配 commit 类型的行(包括 commit, commit (merge) 等)
# 注意message 部分可能包含中文,使用 .* 匹配
pattern = r'^([a-f0-9]{40}) ([a-f0-9]{40}) (.+) <([^>]+)> (\d+) ([+-]\d{4})\tcommit.*?: (.+)$'
for line in log_content.strip().split('\n'):
line = line.strip()
if not line:
continue
match = re.match(pattern, line)
if match:
old_hash, new_hash, author_name, author_email, timestamp, tz_offset, message = match.groups()
# 解析时间戳
ts = int(timestamp)
# 解析时区偏移
tz_hours = int(tz_offset[:3])
tz_mins = int(tz_offset[0] + tz_offset[3:5])
tz = timezone(timedelta(hours=tz_hours, minutes=tz_mins))
dt = datetime.fromtimestamp(ts, tz)
commits.append({
'old_hash': old_hash,
'new_hash': new_hash,
'short_hash': new_hash[:7],
'author_name': author_name,
'author_email': author_email,
'timestamp': ts,
'datetime': dt,
'date_str': dt.strftime('%Y-%m-%d'),
'time_str': dt.strftime('%H:%M:%S'),
'timezone': tz_offset,
'message': message.strip()
})
return commits
def analyze_commit_type(message):
"""
分析提交类型
支持的类型:
- feat: 新功能
- fix: 修复
- docs: 文档
- style: 格式
- refactor: 重构
- perf: 性能优化
- test: 测试
- chore: 构建/工具
- ci: CI/CD
- revert: 回滚
- change/changed: 变更
- remove/removed: 移除
"""
message_lower = message.lower()
# 定义类型映射
type_patterns = [
(r'^feat[.:\s]', 'feat', '新功能 (Feature)', '添加新功能或特性'),
(r'^fix[.:\s]', 'fix', '修复 (Bug Fix)', '修复问题或缺陷'),
(r'^docs[.:\s]', 'docs', '文档 (Documentation)', '文档更新'),
(r'^style[.:\s]', 'style', '格式 (Style)', '代码格式调整'),
(r'^refactor[.:\s]', 'refactor', '重构 (Refactor)', '代码重构'),
(r'^perf[.:\s]', 'perf', '性能优化 (Performance)', '性能改进'),
(r'^test[.:\s]', 'test', '测试 (Test)', '测试相关'),
(r'^chore[.:\s]', 'chore', '构建/工具 (Chore)', '构建流程或工具更新'),
(r'^ci[.:\s]', 'ci', 'CI/CD', '持续集成/部署'),
(r'^revert[.:\s]', 'revert', '回滚 (Revert)', '撤销之前的提交'),
(r'^change[d]?[.:\s]', 'change', '变更 (Change)', '功能或行为变更'),
(r'^remove[d]?[.:\s]', 'remove', '移除 (Remove)', '删除代码或功能'),
(r'^update[.:\s]', 'update', '更新 (Update)', '更新依赖或配置'),
(r'^add[.:\s]', 'add', '添加 (Add)', '添加新内容'),
(r'^introduce[.:\s]', 'introduce', '引入 (Introduce)', '引入新模块或概念'),
(r'^support[.:\s]', 'support', '支持 (Support)', '增加支持'),
(r'^migrate[.:\s]', 'migrate', '迁移 (Migrate)', '迁移或升级'),
(r'^bump[.:\s]', 'bump', '版本升级 (Bump)', '依赖版本升级'),
(r'^enable[.:\s]', 'enable', '启用 (Enable)', '启用功能'),
(r'^use[.:\s]', 'use', '使用 (Use)', '使用某技术或方法'),
(r'^make[.:\s]', 'make', '调整 (Make)', '调整实现'),
(r'^lock[.:\s]', 'lock', '锁定 (Lock)', '锁定特定行为'),
(r'^stamp[.:\s]', 'stamp', '标记 (Stamp)', '版本标记'),
(r'^harden[.:\s]', 'harden', '加固 (Harden)', '安全性/稳定性加固'),
(r'^resolve[.:\s]', 'resolve', '解决 (Resolve)', '解决问题'),
(r'^simplify[.:\s]', 'simplify', '简化 (Simplify)', '简化实现'),
(r'^move[.:\s]', 'move', '移动 (Move)', '文件或代码移动'),
(r'^rebuild[.:\s]', 'rebuild', '重建 (Rebuild)', '重建系统或流程'),
(r'^refresh[.:\s]', 'refresh', '刷新 (Refresh)', '刷新内容'),
(r'^normalize[.:\s]', 'normalize', '规范化 (Normalize)', '规范化处理'),
(r'^redesign[.:\s]', 'redesign', '重新设计 (Redesign)', 'UI/架构重新设计'),
]
for pattern, code, name, description in type_patterns:
if re.match(pattern, message_lower):
return {
'code': code,
'name': name,
'description': description
}
# 版本号提交(如 0.7.9.1, 0.8.0 等)
if re.match(r'^\d+\.\d+', message):
return {
'code': 'release',
'name': '版本发布 (Release)',
'description': '版本号更新或发布'
}
# 默认类型
return {
'code': 'other',
'name': '其他 (Other)',
'description': '其他类型的提交'
}
def generate_commit_markdown(commit):
"""为单个提交生成 Markdown 文档"""
commit_type = analyze_commit_type(commit['message'])
# 提取提交摘要第一行或前50个字符
summary = commit['message'].split('\n')[0][:100]
# 生成分析内容
md_content = f"""# 提交分析报告
## 1. 提交基本信息
| 属性 | 值 |
|------|-----|
| **完整哈希** | `{commit['new_hash']}` |
| **短哈希** | `{commit['short_hash']}` |
| **作者** | {commit['author_name']} <{commit['author_email']}> |
| **提交日期** | {commit['date_str']} |
| **提交时间** | {commit['time_str']} |
| **时区** | {commit['timezone']} |
| **父提交** | `{commit['old_hash']}` |
## 2. 提交信息摘要
```
{commit['message']}
```
**摘要**: {summary}
## 3. 变更类型分析
| 属性 | 值 |
|------|-----|
| **类型代码** | `{commit_type['code']}` |
| **类型名称** | {commit_type['name']} |
| **类型说明** | {commit_type['description']} |
## 4. 提交内容解读
"""
# 根据提交类型添加解读内容
if commit_type['code'] == 'feat':
md_content += f"""
这是一个**新功能**提交,引入了新的功能或特性。
**可能涉及的变更**:
- 新增功能模块或组件
- 新增 API 接口
- 新增用户界面元素
- 新增配置选项
**建议关注**:
- 新功能的实现方式
- 是否包含相应的测试用例
- 文档是否同步更新
"""
elif commit_type['code'] == 'fix':
md_content += f"""
这是一个**问题修复**提交,修复了系统中的某个问题或缺陷。
**可能涉及的变更**:
- 修复程序错误 (Bug)
- 修复 UI 显示问题
- 修复性能问题
- 修复兼容性问题
**建议关注**:
- 修复的问题描述
- 修复方案是否合理
- 是否引入了回归风险
"""
elif commit_type['code'] == 'docs':
md_content += f"""
这是一个**文档更新**提交,更新了项目文档。
**可能涉及的变更**:
- README 更新
- API 文档更新
- 注释完善
- 新增文档文件
**建议关注**:
- 文档内容准确性
- 文档格式规范性
"""
elif commit_type['code'] == 'refactor':
md_content += f"""
这是一个**代码重构**提交,对代码进行了重构优化。
**可能涉及的变更**:
- 代码结构优化
- 提取公共方法
- 重命名变量/类
- 消除重复代码
**建议关注**:
- 重构是否保持功能一致性
- 代码可读性是否提升
"""
elif commit_type['code'] == 'ci':
md_content += f"""
这是一个**CI/CD**提交,更新了持续集成/部署配置。
**可能涉及的变更**:
- GitHub Actions 工作流更新
- 构建脚本调整
- 发布流程优化
- 自动化测试配置
**建议关注**:
- CI 流程是否正常执行
- 部署流程是否受影响
"""
elif commit_type['code'] == 'release':
md_content += f"""
这是一个**版本发布**提交,标记了版本号更新。
**版本号**: {commit['message']}
**可能涉及的变更**:
- 版本号更新
- 发布打包
- 变更日志更新
- 标签创建
**建议关注**:
- 版本号是否符合语义化版本规范
- 变更日志是否完整
"""
elif commit_type['code'] == 'chore':
md_content += f"""
这是一个**构建/工具**提交,更新了构建流程或开发工具。
**可能涉及的变更**:
- 依赖包更新
- 构建配置调整
- 开发工具配置
- 脚本文件更新
**建议关注**:
- 构建是否正常
- 依赖兼容性
"""
elif commit_type['code'] == 'change':
md_content += f"""
这是一个**功能变更**提交,修改了现有功能的行为或实现。
**可能涉及的变更**:
- 功能行为调整
- 配置项变更
- 接口变更
- 默认值修改
**建议关注**:
- 变更是否向后兼容
- 是否需要更新文档
"""
else:
md_content += f"""
这是一个**{commit_type['name']}**提交。
**提交内容**:
{commit['message']}
**建议**:
- 查看具体代码变更以了解详细内容
- 结合项目上下文理解提交意图
"""
# 添加页脚
md_content += f"""
---
*此报告由自动提交分析工具生成*
*生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
"""
return md_content
def main():
"""主函数"""
# 项目根目录
repo_root = Path('d:/github/LanMountainDesktop')
# 读取 HEAD 日志文件
head_log_path = repo_root / '.git' / 'logs' / 'HEAD'
output_dir = repo_root / 'docs' / 'auto_commit_md'
print(f"读取日志文件: {head_log_path}")
if not head_log_path.exists():
print(f"错误: 日志文件不存在: {head_log_path}")
return
with open(head_log_path, 'r', encoding='utf-8') as f:
log_content = f.read()
# 解析提交记录
commits = parse_head_log(log_content)
print(f"解析到 {len(commits)} 个 commit 类型提交")
# 确保输出目录存在
output_dir.mkdir(parents=True, exist_ok=True)
# 统计信息
generated_count = 0
skipped_count = 0
error_count = 0
# 为每个提交生成 Markdown 文件
for commit in commits:
# 文件名格式: YYYYMMDD_<short_hash>.md
filename = f"{commit['date_str'].replace('-', '')}_{commit['short_hash']}.md"
filepath = output_dir / filename
# 如果文件已存在,跳过
if filepath.exists():
print(f"跳过 (已存在): {filename}")
skipped_count += 1
continue
try:
# 生成 Markdown 内容
md_content = generate_commit_markdown(commit)
# 写入文件
with open(filepath, 'w', encoding='utf-8') as f:
f.write(md_content)
print(f"生成: {filename} - {commit['message'][:50]}")
generated_count += 1
except Exception as e:
print(f"错误: 生成 {filename} 失败: {e}")
error_count += 1
# 打印统计信息
print("\n" + "="*50)
print("生成完成!")
print(f" - 新生成: {generated_count} 个文件")
print(f" - 已跳过: {skipped_count} 个文件")
print(f" - 错误: {error_count} 个文件")
print(f" - 总计: {len(commits)} 个提交")
print(f"\n输出目录: {output_dir}")
if __name__ == '__main__':
main()