mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
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:
552
scripts/Analyze-GitCommits.ps1
Normal file
552
scripts/Analyze-GitCommits.ps1
Normal 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
|
||||
662
scripts/GitCommitAnalyzer.cs
Normal file
662
scripts/GitCommitAnalyzer.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
600
scripts/analyze_git_commits.py
Normal file
600
scripts/analyze_git_commits.py
Normal 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()
|
||||
389
scripts/generate_commit_docs.py
Normal file
389
scripts/generate_commit_docs.py
Normal 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()
|
||||
Reference in New Issue
Block a user