Initial commit - MarketAlly.GitCommitEditor library

This commit is contained in:
2025-12-28 09:49:58 +00:00
commit fc4bcf7b8c
70 changed files with 15602 additions and 0 deletions

72
.gitignore vendored Normal file
View File

@@ -0,0 +1,72 @@
# Build results
[Dd]ebug/
[Rr]elease/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
[Oo]ut/
# Visual Studio
.vs/
*.suo
*.user
*.userosscache
*.sln.docstates
*.rsuser
*.userprefs
launchSettings.json
# Rider
.idea/
# VS Code
.vscode/
# NuGet
*.nupkg
*.snupkg
**/[Pp]ackages/*
!**/[Pp]ackages/build/
*.nuget.props
*.nuget.targets
project.lock.json
project.fragment.lock.json
artifacts/
# MSBuild
*.log
*.binlog
msbuild.binlog
# Test results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
TestResult.xml
nunit-*.xml
coverage/
*.coverage
*.coveragexml
# macOS
.DS_Store
*.dmg
._*
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
# Secrets
appsettings.*.json
!appsettings.json
secrets.json
*.pfx
*.key

1038
API_Reference.md Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,70 @@
using Microsoft.Extensions.DependencyInjection;
using MarketAlly.GitCommitEditor.Models.HistoryHealth;
using MarketAlly.GitCommitEditor.Options;
using MarketAlly.GitCommitEditor.Rewriters;
using MarketAlly.GitCommitEditor.Services;
namespace MarketAlly.GitCommitEditor.Extensions;
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds GitMessageImprover services to the service collection.
/// </summary>
public static IServiceCollection AddGitMessageImprover(
this IServiceCollection services,
Action<GitImproverOptions> configureOptions)
{
var options = new GitImproverOptions();
configureOptions(options);
options.ValidateAndThrow();
services.AddSingleton(options);
services.AddSingleton(options.Rules);
services.AddSingleton(options.Ai);
services.AddSingleton<IStateRepository>(sp =>
new FileStateRepository(options.StateFilePath));
// Cost tracking service (singleton to accumulate costs across session)
// Note: App should register ICostPersistenceProvider before calling AddGitMessageImprover
// for persistence support, or costs will only be tracked for the current session
services.AddSingleton<ICostTrackingService>(sp =>
{
var persistence = sp.GetService<ICostPersistenceProvider>();
return new CostTrackingService(persistence);
});
services.AddSingleton<ICommitMessageAnalyzer, CommitMessageAnalyzer>();
services.AddSingleton<IGitOperationsService, GitOperationsService>();
// Use DynamicCommitRewriter which switches between AI and Mock at runtime
// based on whether API key is configured
services.AddSingleton<ICommitMessageRewriter>(sp => new DynamicCommitRewriter(
sp.GetRequiredService<AiOptions>(),
sp.GetRequiredService<ICostTrackingService>()));
// History health analysis services
services.AddSingleton<HealthScoringWeights>();
services.AddSingleton<ICommitAnalyzer>(sp =>
new CommitAnalyzer(sp.GetRequiredService<CommitMessageRules>()));
services.AddSingleton<IHistoryHealthAnalyzer, HistoryHealthAnalyzer>();
services.AddSingleton<IHealthReportGenerator, HealthReportGenerator>();
// Cleanup executor
services.AddSingleton<ICleanupExecutor>(sp => new CleanupExecutor(
sp.GetRequiredService<IGitOperationsService>(),
sp.GetRequiredService<ICommitMessageAnalyzer>(),
sp.GetRequiredService<ICommitMessageRewriter>()));
// Git diagnostic service (AI-powered issue diagnosis)
services.AddSingleton<IGitDiagnosticService>(sp =>
new GitDiagnosticService(
sp.GetRequiredService<AiOptions>(),
sp.GetRequiredService<ICostTrackingService>()));
services.AddSingleton<IGitMessageImproverService, GitMessageImproverService>();
return services;
}
}

View File

@@ -0,0 +1,94 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PackageOutputPath>C:\Users\logik\Dropbox\Nugets</PackageOutputPath>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>MarketAlly.GitCommitEditor</PackageId>
<Title>MarketAlly Git Commit Editor - AI-Powered Commit Message Improvement</Title>
<Version>1.0.0</Version>
<Authors>David H Friedel Jr</Authors>
<Company>MarketAlly</Company>
<Description>A production-ready .NET 9 library for analyzing, improving, and rewriting git commit messages. Features include quality scoring against configurable rules, AI-powered suggestions via Claude/OpenAI/Gemini/Mistral/Qwen, commit history rewriting, push operations, and full DI support.</Description>
<Copyright>Copyright © MarketAlly 2025</Copyright>
<PackageIcon>icon.png</PackageIcon>
<PackageTags>git;commit;message;ai;claude;openai;gemini;libgit2;conventional-commits;code-quality;devtools;dotnet9;net9</PackageTags>
<PackageProjectUrl>https://github.com/MarketAlly/GitCommitEditor</PackageProjectUrl>
<RepositoryUrl>https://github.com/MarketAlly/GitCommitEditor</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageReleaseNotes>
Version 1.0.0 - Initial Release:
- Commit message quality analysis with configurable rules
- AI-powered suggestion generation (Claude, OpenAI, Gemini, Mistral, Qwen)
- Repository discovery and management
- Commit rewriting (amend latest or reword older commits)
- Push operations with force push support
- LRU repository cache with TTL
- State persistence between sessions
- Full dependency injection support
- Interface segregation (IRepositoryManager, ICommitAnalysisService, ISuggestionService, ICommitRewriteService, IGitPushService)
</PackageReleaseNotes>
<RequireLicenseAcceptance>false</RequireLicenseAcceptance>
<!-- Additional NuGet metadata -->
<Summary>Analyze, improve, and rewrite git commit messages using configurable rules and AI-powered suggestions from Claude, OpenAI, Gemini, Mistral, or Qwen.</Summary>
<NeutralLanguage>en-US</NeutralLanguage>
<!-- Documentation and symbols for better debugging -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MarketAlly.AIPlugin" Version="2.6.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MarketAlly.LibGit2Sharp\MarketAlly.LibGit2Sharp.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="icon.png">
<Pack>true</Pack>
<PackagePath>\</PackagePath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>true</Visible>
</None>
<None Include="README.md">
<Pack>true</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="API_Reference.md">
<Pack>true</Pack>
<PackagePath>docs\</PackagePath>
</None>
</ItemGroup>
<!-- Localization Resources -->
<ItemGroup>
<EmbeddedResource Update="Resources\Strings\LibStrings.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>LibStrings.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Strings\LibStrings.*.resx">
<DependentUpon>LibStrings.resx</DependentUpon>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\Strings\LibStrings.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>LibStrings.resx</DependentUpon>
</Compile>
</ItemGroup>
</Project>

10
Models/BatchResult.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace MarketAlly.GitCommitEditor.Models;
public sealed class BatchResult
{
public int TotalProcessed { get; init; }
public int Successful { get; init; }
public int Failed { get; init; }
public int Skipped { get; init; }
public IReadOnlyList<RewriteOperation> Operations { get; init; } = [];
}

View File

@@ -0,0 +1,53 @@
namespace MarketAlly.GitCommitEditor.Models;
/// <summary>
/// Result of batch AI suggestion generation.
/// </summary>
public class BatchSuggestionResult
{
/// <summary>
/// All analyses that were processed.
/// </summary>
public IReadOnlyList<CommitAnalysis> Analyses { get; init; } = Array.Empty<CommitAnalysis>();
/// <summary>
/// Number of commits that got successful AI suggestions.
/// </summary>
public int SuccessCount { get; init; }
/// <summary>
/// Number of commits where AI failed to generate a different suggestion.
/// </summary>
public int FailedCount { get; init; }
/// <summary>
/// Details of each failure for logging/display.
/// </summary>
public IReadOnlyList<SuggestionFailure> Failures { get; init; } = Array.Empty<SuggestionFailure>();
}
/// <summary>
/// Details of a single suggestion failure.
/// </summary>
public class SuggestionFailure
{
/// <summary>
/// The commit hash (short form).
/// </summary>
public string CommitHash { get; init; } = string.Empty;
/// <summary>
/// The original message that couldn't be improved.
/// </summary>
public string OriginalMessage { get; init; } = string.Empty;
/// <summary>
/// Why the suggestion failed.
/// </summary>
public string Reason { get; init; } = string.Empty;
/// <summary>
/// Raw AI response for debugging (if available).
/// </summary>
public string? RawResponse { get; init; }
}

39
Models/BranchInfo.cs Normal file
View File

@@ -0,0 +1,39 @@
using System.Collections.ObjectModel;
namespace MarketAlly.GitCommitEditor.Models;
/// <summary>
/// Represents a branch in a Git repository for display in TreeView.
/// </summary>
public class BranchInfo
{
public string Name { get; init; } = string.Empty;
public string FullName { get; init; } = string.Empty;
public bool IsRemote { get; init; }
public bool IsCurrentHead { get; init; }
public string? LastCommitSha { get; init; }
public DateTimeOffset? LastCommitDate { get; init; }
public string? RemoteName { get; init; }
}
/// <summary>
/// Hierarchical node for TreeView display - can represent a repo or a branch category.
/// </summary>
public class BranchTreeNode
{
public string Name { get; set; } = string.Empty;
public string? Icon { get; set; }
public bool IsExpanded { get; set; } = true;
public BranchInfo? Branch { get; set; }
public ObservableCollection<BranchTreeNode> Children { get; set; } = new();
/// <summary>
/// Display name including icon for current branch indicator
/// </summary>
public string DisplayName => Branch?.IsCurrentHead == true ? $"* {Name}" : Name;
/// <summary>
/// Whether this node represents an actual branch (leaf node)
/// </summary>
public bool IsBranch => Branch != null;
}

23
Models/CommitAnalysis.cs Normal file
View File

@@ -0,0 +1,23 @@
namespace MarketAlly.GitCommitEditor.Models;
public sealed class CommitAnalysis
{
public required string RepoId { get; init; }
public required string RepoName { get; init; }
public required string RepoPath { get; init; }
public required string CommitHash { get; init; }
public string ShortHash => CommitHash.Length >= 7 ? CommitHash[..7] : CommitHash;
public required string OriginalMessage { get; init; }
public string? SuggestedMessage { get; set; }
public required DateTimeOffset CommitDate { get; init; }
public required string Author { get; init; }
public required string AuthorEmail { get; init; }
public required MessageQualityScore Quality { get; init; }
public IReadOnlyList<string> FilesChanged { get; init; } = [];
public string? DiffSummary { get; init; }
public IReadOnlyDictionary<string, string> FileDiffs { get; init; } = new Dictionary<string, string>();
public int LinesAdded { get; init; }
public int LinesDeleted { get; init; }
public AnalysisStatus Status { get; set; } = AnalysisStatus.Pending;
public bool IsLatestCommit { get; init; }
}

17
Models/CommitContext.cs Normal file
View File

@@ -0,0 +1,17 @@
namespace MarketAlly.GitCommitEditor.Models;
/// <summary>
/// Context about the commit's actual changes for smarter analysis.
/// </summary>
public sealed class CommitContext
{
public int FilesChanged { get; init; }
public int LinesAdded { get; init; }
public int LinesDeleted { get; init; }
public IReadOnlyList<string> FileNames { get; init; } = [];
public static CommitContext Empty => new();
public int TotalLinesChanged => LinesAdded + LinesDeleted;
public bool HasSignificantChanges => FilesChanged > 0 || TotalLinesChanged > 0;
}

26
Models/Enums.cs Normal file
View File

@@ -0,0 +1,26 @@
namespace MarketAlly.GitCommitEditor.Models;
public enum IssueSeverity
{
Info,
Warning,
Error
}
public enum AnalysisStatus
{
Pending,
Analyzed,
Approved,
Applied,
Skipped,
Failed
}
public enum OperationStatus
{
Pending,
Applied,
Failed,
RolledBack
}

View File

@@ -0,0 +1,207 @@
using MarketAlly.GitCommitEditor.Resources;
namespace MarketAlly.GitCommitEditor.Models.HistoryHealth;
/// <summary>
/// Overall health grade for a repository.
/// </summary>
public enum HealthGrade
{
/// <summary>90-100: Best practices followed.</summary>
Excellent,
/// <summary>70-89: Minor issues, generally healthy.</summary>
Good,
/// <summary>50-69: Noticeable issues, needs attention.</summary>
Fair,
/// <summary>30-49: Significant problems, cleanup recommended.</summary>
Poor,
/// <summary>0-29: Severe issues, immediate action required.</summary>
Critical
}
/// <summary>
/// Type of duplicate commit detected.
/// </summary>
public enum DuplicateType
{
/// <summary>Same tree SHA (identical content).</summary>
ExactTree,
/// <summary>Same message, different trees.</summary>
ExactMessage,
/// <summary>Similar messages (fuzzy match).</summary>
FuzzyMessage,
/// <summary>Same patch ID (cherry-picked).</summary>
CherryPick,
/// <summary>Same commit rebased.</summary>
RebasedCommit
}
/// <summary>
/// Branch topology classification.
/// </summary>
public enum BranchTopologyType
{
/// <summary>Minimal branching, mostly linear.</summary>
Linear,
/// <summary>Standard develop + release branches.</summary>
GitFlow,
/// <summary>Healthy feature branches.</summary>
Balanced,
/// <summary>Excessive cross-merges.</summary>
Tangled,
/// <summary>Critical complexity.</summary>
Spaghetti
}
/// <summary>
/// Trend direction for metrics over time.
/// </summary>
public enum TrendDirection
{
Improving,
Stable,
Declining
}
/// <summary>
/// Severity of a health issue.
/// </summary>
public enum HealthIssueSeverity
{
Info,
Warning,
Error,
Critical
}
/// <summary>
/// Level of automation for cleanup operations.
/// </summary>
public enum CleanupAutomationLevel
{
/// <summary>Can run with one click.</summary>
FullyAutomated,
/// <summary>Requires user review/approval.</summary>
SemiAutomated,
/// <summary>Requires manual git commands.</summary>
Manual
}
/// <summary>
/// Type of cleanup operation.
/// </summary>
public enum CleanupType
{
SquashDuplicates,
RewordMessages,
SquashMerges,
RebaseLinearize,
ArchiveBranches,
FixAuthorship,
ConsolidateMerges
}
/// <summary>
/// Risk level for a cleanup operation.
/// </summary>
public enum RiskLevel
{
/// <summary>Safe, no history changes.</summary>
None,
/// <summary>Message-only changes.</summary>
Low,
/// <summary>Squashing, requires force push.</summary>
Medium,
/// <summary>Structural changes, potential conflicts.</summary>
High,
/// <summary>Major rewrite, backup required.</summary>
VeryHigh
}
/// <summary>
/// Status of a cleanup operation.
/// </summary>
public enum CleanupOperationStatus
{
Suggested,
Approved,
InProgress,
Completed,
Failed,
Skipped
}
/// <summary>
/// Estimated effort for a task.
/// </summary>
public enum EstimatedEffort
{
/// <summary>Less than 1 hour.</summary>
Minimal,
/// <summary>1-4 hours.</summary>
Low,
/// <summary>1-2 days.</summary>
Medium,
/// <summary>More than 2 days.</summary>
High,
/// <summary>More than 1 week.</summary>
VeryHigh
}
/// <summary>
/// Analysis depth for history scanning.
/// </summary>
public enum AnalysisDepth
{
/// <summary>Sample 200 commits, basic metrics only.</summary>
Quick,
/// <summary>1000 commits, all metrics.</summary>
Standard,
/// <summary>5000 commits, comprehensive.</summary>
Deep,
/// <summary>All commits (slow for large repos).</summary>
Full
}
/// <summary>
/// Report output format.
/// </summary>
public enum ReportFormat
{
Json,
Markdown,
Html,
Console
}
public static class HealthGradeExtensions
{
public static string GetDescription(this HealthGrade grade) => grade switch
{
HealthGrade.Excellent => "Repository follows git best practices. Minimal cleanup needed.",
HealthGrade.Good => "Repository is generally healthy with minor issues.",
HealthGrade.Fair => Str.HealthStatus_NeedsAttention,
HealthGrade.Poor => "Repository has significant problems. Cleanup recommended.",
HealthGrade.Critical => Str.HealthStatus_Critical,
_ => "Unknown"
};
public static string GetIcon(this HealthGrade grade) => grade switch
{
HealthGrade.Excellent => "✅",
HealthGrade.Good => "👍",
HealthGrade.Fair => "⚠️",
HealthGrade.Poor => "❌",
HealthGrade.Critical => "🚨",
_ => "❓"
};
public static HealthGrade FromScore(int score) => score switch
{
>= 90 => HealthGrade.Excellent,
>= 70 => HealthGrade.Good,
>= 50 => HealthGrade.Fair,
>= 30 => HealthGrade.Poor,
_ => HealthGrade.Critical
};
}

View File

@@ -0,0 +1,144 @@
namespace MarketAlly.GitCommitEditor.Models.HistoryHealth;
/// <summary>
/// Represents a group of duplicate commits.
/// </summary>
public sealed class DuplicateCommitGroup
{
public required string CanonicalMessage { get; init; }
public required IReadOnlyList<string> CommitHashes { get; init; }
public required DuplicateType Type { get; init; }
public int InstanceCount => CommitHashes.Count;
}
/// <summary>
/// Metrics for duplicate commit detection.
/// </summary>
public sealed class DuplicateCommitMetrics
{
public int TotalCommitsAnalyzed { get; init; }
public int TotalDuplicateGroups { get; init; }
public int TotalDuplicateInstances { get; init; }
public int ExactDuplicates { get; init; }
public int CherryPicks { get; init; }
public int FuzzyMatches { get; init; }
public IReadOnlyList<DuplicateCommitGroup> DuplicateGroups { get; init; } = [];
public double DuplicateRatio => TotalCommitsAnalyzed > 0
? (double)TotalDuplicateInstances / TotalCommitsAnalyzed * 100
: 0;
}
/// <summary>
/// Metrics for merge commit analysis.
/// </summary>
public sealed class MergeCommitMetrics
{
public int TotalCommits { get; init; }
public int TotalMerges { get; init; }
public int TrivialMerges { get; init; }
public int ConflictMerges { get; init; }
public int MergeFixCommits { get; init; }
public int NullMerges { get; init; }
public double AverageMergeComplexity { get; init; }
public IReadOnlyList<string> MessyMergePatterns { get; init; } = [];
public IReadOnlyList<string> MergeFixCommitHashes { get; init; } = [];
public int MergeRatio => TotalCommits > 0
? (int)Math.Round((double)TotalMerges / TotalCommits * 100)
: 0;
}
/// <summary>
/// Metrics for branch complexity analysis.
/// </summary>
public sealed class BranchComplexityMetrics
{
public int TotalBranches { get; init; }
public int ActiveBranches { get; init; }
public int StaleBranches { get; init; }
public int CrossMerges { get; init; }
public double AverageBranchAge { get; init; }
public double AverageBranchLength { get; init; }
public int LongLivedBranches { get; init; }
public BranchTopologyType Topology { get; init; }
public IReadOnlyList<string> StaleBranchNames { get; init; } = [];
}
/// <summary>
/// A cluster of commits with similar quality scores.
/// </summary>
public sealed class QualityCluster
{
public DateTimeOffset StartDate { get; init; }
public DateTimeOffset EndDate { get; init; }
public int CommitCount { get; init; }
public double AverageScore { get; init; }
public string? PossibleCause { get; init; }
}
/// <summary>
/// Distribution of commit message quality scores.
/// </summary>
public sealed class MessageQualityDistribution
{
public int TotalCommits { get; init; }
public int Excellent { get; init; }
public int Good { get; init; }
public int Fair { get; init; }
public int Poor { get; init; }
public double AverageScore { get; init; }
public double MedianScore { get; init; }
public double StandardDeviation { get; init; }
public TrendDirection Trend { get; init; }
public IReadOnlyList<QualityCluster> Clusters { get; init; } = [];
public IReadOnlyList<string> PoorCommitHashes { get; init; } = [];
}
/// <summary>
/// Statistics for a single author.
/// </summary>
public sealed class AuthorStats
{
public required string Name { get; init; }
public required string Email { get; init; }
public int CommitCount { get; init; }
public double AverageMessageQuality { get; init; }
public int MergeCommitCount { get; init; }
}
/// <summary>
/// Metrics for authorship analysis.
/// </summary>
public sealed class AuthorshipMetrics
{
public int TotalAuthors { get; init; }
public int TotalCommits { get; init; }
public int MissingEmailCount { get; init; }
public int InvalidEmailCount { get; init; }
public int BotCommits { get; init; }
public IReadOnlyDictionary<string, AuthorStats> AuthorBreakdown { get; init; }
= new Dictionary<string, AuthorStats>();
}
/// <summary>
/// Component scores breakdown.
/// </summary>
public sealed class ComponentScores
{
public int DuplicateScore { get; init; }
public int MergeScore { get; init; }
public int BranchScore { get; init; }
public int MessageScore { get; init; }
public int AuthorshipScore { get; init; }
}
/// <summary>
/// Overall health score with breakdown.
/// </summary>
public sealed class HealthScore
{
public int OverallScore { get; init; }
public HealthGrade Grade { get; init; }
public required ComponentScores ComponentScores { get; init; }
}

View File

@@ -0,0 +1,148 @@
namespace MarketAlly.GitCommitEditor.Models.HistoryHealth;
/// <summary>
/// A health issue detected in the repository.
/// </summary>
public sealed class HealthIssue
{
public required string Code { get; init; }
public required string Category { get; init; }
public required HealthIssueSeverity Severity { get; init; }
public required string Title { get; init; }
public required string Description { get; init; }
public int ImpactScore { get; init; }
public IReadOnlyList<string> AffectedCommits { get; init; } = [];
}
/// <summary>
/// A recommendation for improving repository health.
/// </summary>
public sealed class HealthRecommendation
{
public required string Category { get; init; }
public required string Title { get; init; }
public required string Description { get; init; }
public required string Action { get; init; }
public string? Rationale { get; init; }
public int PriorityScore { get; init; }
public EstimatedEffort Effort { get; init; }
public int ExpectedScoreImprovement { get; init; }
}
/// <summary>
/// A cleanup operation that can be performed.
/// </summary>
public sealed class CleanupOperation
{
public required string Id { get; init; }
public required string Title { get; init; }
public required string Description { get; init; }
public required CleanupType Type { get; init; }
public required CleanupAutomationLevel AutomationLevel { get; init; }
public EstimatedEffort Effort { get; init; }
public RiskLevel Risk { get; init; }
public int ExpectedScoreImprovement { get; init; }
public IReadOnlyList<string> AffectedCommits { get; init; } = [];
public string? GitCommand { get; init; }
public CleanupOperationStatus Status { get; set; } = CleanupOperationStatus.Suggested;
}
/// <summary>
/// Cleanup suggestions organized by automation level.
/// </summary>
public sealed class CleanupSuggestions
{
public IReadOnlyList<CleanupOperation> AutomatedOperations { get; init; } = [];
public IReadOnlyList<CleanupOperation> SemiAutomatedOperations { get; init; } = [];
public IReadOnlyList<CleanupOperation> ManualOperations { get; init; } = [];
public int TotalOperations =>
AutomatedOperations.Count + SemiAutomatedOperations.Count + ManualOperations.Count;
public int TotalExpectedImprovement =>
AutomatedOperations.Sum(o => o.ExpectedScoreImprovement) +
SemiAutomatedOperations.Sum(o => o.ExpectedScoreImprovement) +
ManualOperations.Sum(o => o.ExpectedScoreImprovement);
}
/// <summary>
/// Complete history health analysis results.
/// </summary>
public sealed class HistoryHealthAnalysis
{
public required string RepoPath { get; init; }
public required string RepoName { get; init; }
public required string CurrentBranch { get; init; }
public DateTimeOffset AnalyzedAt { get; init; } = DateTimeOffset.UtcNow;
public int CommitsAnalyzed { get; init; }
public DateTimeOffset? OldestCommitDate { get; init; }
public DateTimeOffset? NewestCommitDate { get; init; }
// Metrics
public required DuplicateCommitMetrics Duplicates { get; init; }
public required MergeCommitMetrics MergeMetrics { get; init; }
public required BranchComplexityMetrics BranchMetrics { get; init; }
public required MessageQualityDistribution MessageDistribution { get; init; }
public required AuthorshipMetrics AuthorshipMetrics { get; init; }
}
/// <summary>
/// Complete health report with scoring, issues, and recommendations.
/// </summary>
public sealed class HistoryHealthReport
{
public required string RepoId { get; init; }
public required string RepoName { get; init; }
public required string RepoPath { get; init; }
public required string CurrentBranch { get; init; }
public DateTimeOffset GeneratedAt { get; init; } = DateTimeOffset.UtcNow;
public int CommitsAnalyzed { get; init; }
// Summary
public required HealthScore Score { get; init; }
// Detailed metrics
public required DuplicateCommitMetrics DuplicateMetrics { get; init; }
public required MergeCommitMetrics MergeMetrics { get; init; }
public required BranchComplexityMetrics BranchMetrics { get; init; }
public required MessageQualityDistribution MessageDistribution { get; init; }
public required AuthorshipMetrics AuthorshipMetrics { get; init; }
// Issues and recommendations
public IReadOnlyList<HealthIssue> Issues { get; init; } = [];
public IReadOnlyList<HealthRecommendation> Recommendations { get; init; } = [];
// Cleanup opportunities
public CleanupSuggestions? CleanupSuggestions { get; init; }
// Convenience properties
public int CriticalIssueCount => Issues.Count(i => i.Severity == HealthIssueSeverity.Critical);
public int ErrorCount => Issues.Count(i => i.Severity == HealthIssueSeverity.Error);
public int WarningCount => Issues.Count(i => i.Severity == HealthIssueSeverity.Warning);
}
/// <summary>
/// Result of a cleanup operation.
/// </summary>
public sealed class CleanupResult
{
public bool Success { get; init; }
public int CommitsModified { get; init; }
public int CommitsRemoved { get; init; }
public string? BackupBranch { get; init; }
public string? ErrorMessage { get; init; }
public HistoryHealthReport? UpdatedReport { get; init; }
}
/// <summary>
/// Preview of what a cleanup operation will do.
/// </summary>
public sealed class CleanupPreview
{
public int CommitsAffected { get; init; }
public int RefsAffected { get; init; }
public IReadOnlyList<string> CommitsToModify { get; init; } = [];
public IReadOnlyList<string> CommitsToRemove { get; init; } = [];
public int ExpectedScoreImprovement { get; init; }
public string Summary { get; init; } = string.Empty;
}

View File

@@ -0,0 +1,106 @@
using MarketAlly.GitCommitEditor.Resources;
namespace MarketAlly.GitCommitEditor.Models.HistoryHealth;
/// <summary>
/// Options for history health analysis.
/// </summary>
public sealed class HistoryAnalysisOptions
{
/// <summary>
/// Analysis depth - affects number of commits analyzed.
/// </summary>
public AnalysisDepth Depth { get; set; } = AnalysisDepth.Standard;
/// <summary>
/// Maximum commits to analyze. Overrides Depth if set.
/// </summary>
public int? MaxCommitsToAnalyze { get; set; }
/// <summary>
/// Only analyze commits since this date.
/// </summary>
public DateTimeOffset? AnalyzeSince { get; set; }
/// <summary>
/// Include duplicate commit detection.
/// </summary>
public bool IncludeDuplicateDetection { get; set; } = true;
/// <summary>
/// Include branch complexity analysis.
/// </summary>
public bool IncludeBranchAnalysis { get; set; } = true;
/// <summary>
/// Include message quality distribution.
/// </summary>
public bool IncludeMessageDistribution { get; set; } = true;
/// <summary>
/// Branches to exclude from analysis.
/// </summary>
public string[] ExcludeBranches { get; set; } = [];
/// <summary>
/// Generate cleanup suggestions.
/// </summary>
public bool GenerateCleanupSuggestions { get; set; } = true;
/// <summary>
/// Gets the effective max commits based on depth.
/// </summary>
public int EffectiveMaxCommits => MaxCommitsToAnalyze ?? Depth switch
{
AnalysisDepth.Quick => 200,
AnalysisDepth.Standard => 1000,
AnalysisDepth.Deep => 5000,
AnalysisDepth.Full => int.MaxValue,
_ => 1000
};
}
/// <summary>
/// Scoring weights for health calculation.
/// </summary>
public sealed class HealthScoringWeights
{
public double DuplicateWeight { get; set; } = 0.20;
public double MergeWeight { get; set; } = 0.25;
public double BranchWeight { get; set; } = 0.20;
public double MessageWeight { get; set; } = 0.25;
public double AuthorshipWeight { get; set; } = 0.10;
public void Validate()
{
var sum = DuplicateWeight + MergeWeight + BranchWeight + MessageWeight + AuthorshipWeight;
if (Math.Abs(sum - 1.0) > 0.001)
throw new InvalidOperationException(Str.Validation_WeightsSum(sum));
}
public static HealthScoringWeights Default => new();
}
/// <summary>
/// Options for duplicate detection.
/// </summary>
public sealed class DuplicateDetectionOptions
{
public bool DetectExactTreeDuplicates { get; set; } = true;
public bool DetectCherryPicks { get; set; } = true;
public bool DetectFuzzyMatches { get; set; } = true;
public int FuzzyMatchThreshold { get; set; } = 3;
public int TimeWindowMinutes { get; set; } = 5;
}
/// <summary>
/// Progress information during analysis.
/// </summary>
public sealed class AnalysisProgress
{
public string CurrentStage { get; init; } = string.Empty;
public int PercentComplete { get; init; }
public int CommitsProcessed { get; init; }
public int TotalCommits { get; init; }
public string? CurrentItem { get; init; }
}

51
Models/ImproverState.cs Normal file
View File

@@ -0,0 +1,51 @@
namespace MarketAlly.GitCommitEditor.Models;
public sealed class ImproverState
{
private const int DefaultMaxHistorySize = 1000;
private const int DefaultMaxHistoryAgeDays = 90;
public List<ManagedRepo> Repos { get; set; } = [];
public List<RewriteOperation> History { get; set; } = [];
public Dictionary<string, string> LastAnalyzedCommits { get; set; } = [];
public DateTimeOffset LastUpdated { get; set; } = DateTimeOffset.UtcNow;
/// <summary>
/// Prunes history to keep only recent entries within size and age limits.
/// </summary>
/// <param name="maxSize">Maximum number of history entries to retain.</param>
/// <param name="maxAgeDays">Maximum age in days for history entries.</param>
/// <returns>Number of entries removed.</returns>
public int PruneHistory(int maxSize = DefaultMaxHistorySize, int maxAgeDays = DefaultMaxHistoryAgeDays)
{
var initialCount = History.Count;
var cutoffDate = DateTimeOffset.UtcNow.AddDays(-maxAgeDays);
// Remove entries older than max age
History.RemoveAll(h => h.CreatedAt < cutoffDate);
// If still over size limit, keep only the most recent entries
if (History.Count > maxSize)
{
var sorted = History.OrderByDescending(h => h.CreatedAt).ToList();
History.Clear();
History.AddRange(sorted.Take(maxSize));
}
return initialCount - History.Count;
}
/// <summary>
/// Removes history entries for repositories that are no longer registered.
/// </summary>
/// <returns>Number of orphaned entries removed.</returns>
public int RemoveOrphanedHistory()
{
var repoIds = Repos.Select(r => r.Id).ToHashSet();
var initialCount = History.Count;
History.RemoveAll(h => !repoIds.Contains(h.RepoId));
return initialCount - History.Count;
}
}

17
Models/ManagedRepo.cs Normal file
View File

@@ -0,0 +1,17 @@
namespace MarketAlly.GitCommitEditor.Models;
public sealed class ManagedRepo
{
public string Id { get; init; } = Guid.NewGuid().ToString("N")[..8];
public string Name { get; init; } = string.Empty;
public string Path { get; init; } = string.Empty;
public string? RemoteUrl { get; init; }
public string? CurrentBranch { get; init; }
public DateTimeOffset AddedAt { get; init; } = DateTimeOffset.UtcNow;
public DateTimeOffset? LastScannedAt { get; set; }
public DateTimeOffset? LastAnalyzedAt { get; set; }
public int TotalCommits { get; set; }
public int CommitsNeedingImprovement { get; set; }
public override string ToString() => Name;
}

View File

@@ -0,0 +1,8 @@
namespace MarketAlly.GitCommitEditor.Models;
public sealed class MessageQualityScore
{
public int OverallScore { get; init; }
public IReadOnlyList<QualityIssue> Issues { get; init; } = [];
public bool NeedsImprovement => OverallScore < 70 || Issues.Any();
}

8
Models/QualityIssue.cs Normal file
View File

@@ -0,0 +1,8 @@
namespace MarketAlly.GitCommitEditor.Models;
public sealed class QualityIssue
{
public required IssueSeverity Severity { get; init; }
public required string Code { get; init; }
public required string Message { get; init; }
}

57
Models/Results.cs Normal file
View File

@@ -0,0 +1,57 @@
using MarketAlly.GitCommitEditor.Resources;
namespace MarketAlly.GitCommitEditor.Models;
/// <summary>
/// Result of a push operation.
/// </summary>
public sealed record GitPushResult(bool Success, string Message)
{
public static GitPushResult Ok(string? message = null) => new(true, message ?? Str.Service_PushSuccess);
public static GitPushResult Fail(string message) => new(false, message);
}
/// <summary>
/// Tracking information for a branch.
/// </summary>
public sealed record TrackingInfo(
string? UpstreamBranch,
int? AheadBy,
int? BehindBy)
{
public static TrackingInfo None => new(null, null, null);
public bool HasUpstream => UpstreamBranch != null;
public bool IsInSync => AheadBy == 0 && BehindBy == 0;
}
/// <summary>
/// Result of an AI suggestion operation.
/// </summary>
public sealed record SuggestionResult(
CommitAnalysis? Analysis,
string? Suggestion,
string? ErrorMessage = null,
string? RawResponse = null,
bool ReturnedOriginal = false,
int InputTokens = 0,
int OutputTokens = 0,
decimal EstimatedCost = 0)
{
public bool Success => ErrorMessage == null && Suggestion != null;
public int TotalTokens => InputTokens + OutputTokens;
public static SuggestionResult Ok(CommitAnalysis analysis, string suggestion, int inputTokens = 0, int outputTokens = 0, decimal cost = 0)
=> new(analysis, suggestion, InputTokens: inputTokens, OutputTokens: outputTokens, EstimatedCost: cost);
public static SuggestionResult Succeeded(string suggestion, int inputTokens = 0, int outputTokens = 0, decimal cost = 0)
=> new(null, suggestion, InputTokens: inputTokens, OutputTokens: outputTokens, EstimatedCost: cost);
public static SuggestionResult Fail(CommitAnalysis analysis, string error)
=> new(analysis, null, error);
public static SuggestionResult Failed(string error, string? rawResponse = null)
=> new(null, null, error, rawResponse);
public static SuggestionResult FailedWithOriginal(string originalMessage, string? rawResponse = null, int inputTokens = 0, int outputTokens = 0, decimal cost = 0)
=> new(null, originalMessage, Str.Service_AiFallback, rawResponse, ReturnedOriginal: true, InputTokens: inputTokens, OutputTokens: outputTokens, EstimatedCost: cost);
}

View File

@@ -0,0 +1,17 @@
namespace MarketAlly.GitCommitEditor.Models;
public sealed class RewriteOperation
{
public string Id { get; init; } = Guid.NewGuid().ToString("N")[..12];
public required string RepoId { get; init; }
public required string RepoPath { get; init; }
public required string CommitHash { get; init; }
public required string OriginalMessage { get; init; }
public required string NewMessage { get; init; }
public string? NewCommitHash { get; set; }
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
public DateTimeOffset? AppliedAt { get; set; }
public OperationStatus Status { get; set; } = OperationStatus.Pending;
public string? ErrorMessage { get; set; }
public bool IsLatestCommit { get; init; }
}

171
Models/RewriteSafetyInfo.cs Normal file
View File

@@ -0,0 +1,171 @@
using MarketAlly.GitCommitEditor.Resources;
namespace MarketAlly.GitCommitEditor.Models;
/// <summary>
/// Safety information for a batch rewrite operation.
/// </summary>
public sealed class RewriteSafetyInfo
{
/// <summary>
/// Whether the repository has uncommitted changes.
/// </summary>
public bool HasUncommittedChanges { get; init; }
/// <summary>
/// Whether any commits to be rewritten have been pushed to remote.
/// </summary>
public bool HasPushedCommits { get; init; }
/// <summary>
/// Number of commits that have been pushed to remote.
/// </summary>
public int PushedCommitCount { get; init; }
/// <summary>
/// Number of commits that are local only.
/// </summary>
public int LocalOnlyCommitCount { get; init; }
/// <summary>
/// Total commits to be rewritten.
/// </summary>
public int TotalCommitCount { get; init; }
/// <summary>
/// Whether the current branch tracks a remote branch.
/// </summary>
public bool HasRemoteTracking { get; init; }
/// <summary>
/// The remote tracking branch name, if any.
/// </summary>
public string? RemoteTrackingBranch { get; init; }
/// <summary>
/// Number of commits ahead of remote.
/// </summary>
public int? AheadOfRemote { get; init; }
/// <summary>
/// Number of commits behind remote.
/// </summary>
public int? BehindRemote { get; init; }
/// <summary>
/// Whether a backup branch was created.
/// </summary>
public bool BackupBranchCreated { get; init; }
/// <summary>
/// Name of the backup branch, if created.
/// </summary>
public string? BackupBranchName { get; init; }
/// <summary>
/// Whether the operation can proceed safely.
/// </summary>
public bool CanProceedSafely => !HasUncommittedChanges && !HasPushedCommits;
/// <summary>
/// Whether the operation can proceed with warnings.
/// </summary>
public bool CanProceedWithWarnings => !HasUncommittedChanges;
/// <summary>
/// Gets warning messages based on the safety info.
/// </summary>
public IReadOnlyList<string> GetWarnings()
{
var warnings = new List<string>();
if (HasUncommittedChanges)
{
warnings.Add(Str.Safety_UncommittedChanges);
}
if (HasPushedCommits)
{
warnings.Add(Str.Safety_PushedCommits(PushedCommitCount));
}
if (BehindRemote > 0)
{
warnings.Add(Str.Safety_BehindRemote(BehindRemote.Value));
}
return warnings;
}
/// <summary>
/// Gets a summary description of the operation.
/// </summary>
public string GetSummary()
{
var parts = new List<string>();
if (LocalOnlyCommitCount > 0)
{
parts.Add($"{LocalOnlyCommitCount} local commit(s)");
}
if (PushedCommitCount > 0)
{
parts.Add($"{PushedCommitCount} pushed commit(s)");
}
return string.Join(" and ", parts) + " will be rewritten.";
}
}
/// <summary>
/// Result of a batch rewrite execute operation.
/// </summary>
public sealed class BatchRewriteResult
{
/// <summary>
/// Whether the overall operation succeeded.
/// </summary>
public bool Success { get; init; }
/// <summary>
/// Number of commits successfully rewritten.
/// </summary>
public int SuccessCount { get; init; }
/// <summary>
/// Number of commits that failed to rewrite.
/// </summary>
public int FailedCount { get; init; }
/// <summary>
/// Number of commits skipped.
/// </summary>
public int SkippedCount { get; init; }
/// <summary>
/// Error message if the operation failed.
/// </summary>
public string? ErrorMessage { get; init; }
/// <summary>
/// Whether a force push is required.
/// </summary>
public bool RequiresForcePush { get; init; }
/// <summary>
/// The backup branch name if one was created.
/// </summary>
public string? BackupBranchName { get; init; }
/// <summary>
/// Individual operation results.
/// </summary>
public IReadOnlyList<RewriteOperation> Operations { get; init; } = [];
public static BatchRewriteResult Failure(string error) => new()
{
Success = false,
ErrorMessage = error
};
}

65
Options/AiOptions.cs Executable file
View File

@@ -0,0 +1,65 @@
using System.Text.Json.Serialization;
using MarketAlly.AIPlugin;
using MarketAlly.AIPlugin.Conversation;
namespace MarketAlly.GitCommitEditor.Options;
/// <summary>
/// Model information for display in settings UI
/// </summary>
public sealed class ModelDisplayInfo
{
public string ModelId { get; init; } = string.Empty;
public string DisplayName { get; init; } = string.Empty;
public string Tier { get; init; } = string.Empty;
public decimal InputCostPer1MTokens { get; init; }
public decimal OutputCostPer1MTokens { get; init; }
public string Speed { get; init; } = string.Empty;
/// <summary>
/// Formatted display string for picker: "DisplayName (Tier) - $X.XX/1M"
/// </summary>
public string FormattedDisplay => $"{DisplayName} ({Tier}) - ${InputCostPer1MTokens:F2}/1M in";
public override string ToString() => FormattedDisplay;
}
/// <summary>
/// AI configuration options.
/// For provider/model discovery, use IModelProviderService instead of static methods.
/// </summary>
public sealed class AiOptions
{
[JsonIgnore]
public string ApiKey { get; set; } = string.Empty;
/// <summary>
/// AI Provider: Claude, OpenAI, Gemini, Qwen
/// </summary>
public string Provider { get; set; } = "Claude";
/// <summary>
/// Model name specific to the provider
/// </summary>
public string Model { get; set; } = ModelConstants.Claude.Sonnet4;
public bool IncludeDiffContext { get; set; } = true;
public int MaxDiffLines { get; set; } = 200;
public int MaxTokens { get; set; } = 500;
public int RateLimitDelayMs { get; set; } = 500;
/// <summary>
/// Parses a provider string to AIProvider enum.
/// </summary>
public static AIProvider ParseProvider(string? provider)
{
return provider?.ToLowerInvariant() switch
{
"claude" or "anthropic" => AIProvider.Claude,
"openai" or "gpt" => AIProvider.OpenAI,
"gemini" or "google" => AIProvider.Gemini,
"qwen" or "alibaba" => AIProvider.Qwen,
_ => AIProvider.Claude
};
}
}

View File

@@ -0,0 +1,23 @@
namespace MarketAlly.GitCommitEditor.Options;
public sealed class CommitMessageRules
{
public int MinSubjectLength { get; set; } = 10;
public int MaxSubjectLength { get; set; } = 72;
public int MinBodyLength { get; set; } = 0;
public bool RequireConventionalCommit { get; set; } = false;
public bool RequireIssueReference { get; set; } = false;
public string[] BannedPhrases { get; set; } =
[
"fix", "wip", "temp", "asdf", "test", "stuff", "things",
"changes", "update", "updates", "misc", "minor", "oops",
"commit", "save", "checkpoint", "progress", "done", "finished"
];
public string[] ConventionalTypes { get; set; } =
[
"feat", "fix", "docs", "style", "refactor", "perf",
"test", "build", "ci", "chore", "revert"
];
}

View File

@@ -0,0 +1,89 @@
using System.ComponentModel.DataAnnotations;
using MarketAlly.GitCommitEditor.Resources;
namespace MarketAlly.GitCommitEditor.Options;
public sealed class GitImproverOptions : IValidatableObject
{
/// <summary>
/// Root directory to scan for git repositories
/// </summary>
public string WorkspaceRoot { get; set; } = string.Empty;
/// <summary>
/// Path to persist state between sessions
/// </summary>
public string StateFilePath { get; set; } = "git-improver-state.json";
/// <summary>
/// Rules for analyzing commit message quality
/// </summary>
public CommitMessageRules Rules { get; set; } = new();
/// <summary>
/// AI configuration for generating suggestions
/// </summary>
public AiOptions Ai { get; set; } = new();
/// <summary>
/// Maximum number of commits to analyze per repo
/// </summary>
public int MaxCommitsPerRepo { get; set; } = 100;
/// <summary>
/// Only analyze commits newer than this date
/// </summary>
public DateTimeOffset? AnalyzeSince { get; set; }
/// <summary>
/// Skip commits by these authors (useful for excluding bots)
/// </summary>
public string[] ExcludedAuthors { get; set; } = [];
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(WorkspaceRoot))
{
yield return new ValidationResult(
Str.Validation_WorkspaceRequired,
[nameof(WorkspaceRoot)]);
}
else if (!Directory.Exists(WorkspaceRoot))
{
yield return new ValidationResult(
Str.Validation_WorkspaceNotFound(WorkspaceRoot),
[nameof(WorkspaceRoot)]);
}
if (MaxCommitsPerRepo <= 0)
{
yield return new ValidationResult(
Str.Validation_MaxCommitsPositive,
[nameof(MaxCommitsPerRepo)]);
}
if (Rules == null)
{
yield return new ValidationResult(
Str.Validation_RulesNull,
[nameof(Rules)]);
}
if (Ai == null)
{
yield return new ValidationResult(
Str.Validation_AiOptionsNull,
[nameof(Ai)]);
}
}
public void ValidateAndThrow()
{
var results = Validate(new ValidationContext(this)).ToList();
if (results.Count > 0)
{
var messages = string.Join("; ", results.Select(r => r.ErrorMessage));
throw new ValidationException(Str.Validation_InvalidOptions(messages));
}
}
}

View File

@@ -0,0 +1,135 @@
using MarketAlly.AIPlugin;
namespace MarketAlly.GitCommitEditor.Plugins;
/// <summary>
/// Valid conventional commit types
/// </summary>
public enum CommitType
{
/// <summary>New feature</summary>
feat,
/// <summary>Bug fix</summary>
fix,
/// <summary>Documentation only</summary>
docs,
/// <summary>Formatting, whitespace (no code change)</summary>
style,
/// <summary>Code change that neither fixes a bug nor adds a feature</summary>
refactor,
/// <summary>Performance improvement</summary>
perf,
/// <summary>Adding or updating tests</summary>
test,
/// <summary>Build system or dependencies</summary>
build,
/// <summary>CI/CD configuration</summary>
ci,
/// <summary>Maintenance tasks, logging, tooling</summary>
chore
}
/// <summary>
/// Result model for the commit message generation
/// </summary>
public class CommitMessageResult
{
public CommitType Type { get; set; } = CommitType.chore;
public string? Scope { get; set; }
public string Description { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
/// <summary>
/// Gets the formatted subject line: type(scope): description
/// </summary>
public string Subject => string.IsNullOrEmpty(Scope)
? $"{Type}: {Description}"
: $"{Type}({Scope}): {Description}";
}
/// <summary>
/// AI Plugin for generating improved commit messages following conventional commit format
/// </summary>
[AIPlugin("GenerateCommitMessage", "Generates an improved git commit message based on the original message, changed files, and diff content. Returns a structured result with subject line and optional body.")]
public class GenerateCommitMessagePlugin : IAIPlugin
{
[AIParameter("The original commit message to improve", required: true)]
public string OriginalMessage { get; set; } = string.Empty;
[AIParameter("List of file paths that were changed in the commit", required: true)]
public List<string> FilesChanged { get; set; } = new();
[AIParameter("Number of lines added in the commit", required: true)]
public int LinesAdded { get; set; }
[AIParameter("Number of lines deleted in the commit", required: true)]
public int LinesDeleted { get; set; }
[AIParameter("Summary of the diff/changes (truncated)", required: false)]
public string? DiffSummary { get; set; }
[AIParameter("List of quality issues detected in the original message", required: false)]
public List<string>? QualityIssues { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["originalMessage"] = typeof(string),
["filesChanged"] = typeof(List<string>),
["linesAdded"] = typeof(int),
["linesDeleted"] = typeof(int),
["diffSummary"] = typeof(string),
["qualityIssues"] = typeof(List<string>)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new CommitMessageResult
{
Type = CommitType.chore,
Description = OriginalMessage,
Body = string.Empty
};
return Task.FromResult(new AIPluginResult(result, "Commit message generated"));
}
}
/// <summary>
/// AI Plugin that the AI calls to return the generated commit message
/// </summary>
[AIPlugin("ReturnCommitMessage", "Returns the generated commit message. Call this after analyzing the commit to provide the improved message in conventional commit format.")]
public class ReturnCommitMessagePlugin : IAIPlugin
{
[AIParameter("The commit type", required: true)]
public CommitType Type { get; set; } = CommitType.chore;
[AIParameter("Optional scope/area of the codebase affected (e.g., 'api', 'ui', 'auth'). Omit if not applicable.", required: false)]
public string? Scope { get; set; }
[AIParameter("Short description of the change (max 60 chars, imperative mood, no period). Example: 'add user authentication'", required: true)]
public string Description { get; set; } = string.Empty;
[AIParameter("Optional extended description explaining what changed and why. Leave empty if the subject is self-explanatory.", required: false)]
public string Body { get; set; } = string.Empty;
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["type"] = typeof(CommitType),
["scope"] = typeof(string),
["description"] = typeof(string),
["body"] = typeof(string)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new CommitMessageResult
{
Type = Type,
Scope = Scope,
Description = Description,
Body = Body ?? string.Empty
};
return Task.FromResult(new AIPluginResult(result, "Commit message returned"));
}
}

92
Plugins/GitDiagnosticPlugin.cs Executable file
View File

@@ -0,0 +1,92 @@
using MarketAlly.AIPlugin;
namespace MarketAlly.GitCommitEditor.Plugins;
/// <summary>
/// Risk level for git fix operations
/// </summary>
public enum RiskLevel
{
/// <summary>Safe operation, no data loss risk</summary>
Low,
/// <summary>Some risk, review before executing</summary>
Medium,
/// <summary>Potential data loss, use with caution</summary>
High
}
/// <summary>
/// Result model for git issue diagnosis
/// </summary>
public class GitDiagnosisResult
{
/// <summary>
/// Short summary of the problem (1-2 sentences)
/// </summary>
public string Problem { get; set; } = string.Empty;
/// <summary>
/// Explanation of why this happened
/// </summary>
public string Cause { get; set; } = string.Empty;
/// <summary>
/// The recommended fix command(s) to run
/// </summary>
public string FixCommand { get; set; } = string.Empty;
/// <summary>
/// Warning about potential data loss or side effects (if any)
/// </summary>
public string Warning { get; set; } = string.Empty;
/// <summary>
/// Risk level of the fix operation
/// </summary>
public RiskLevel RiskLevel { get; set; } = RiskLevel.Low;
}
/// <summary>
/// AI Plugin that the AI calls to return the git diagnosis
/// </summary>
[AIPlugin("ReturnGitDiagnosis", "Returns the diagnosis of a git issue with the problem summary, cause, fix command, and any warnings.")]
public class ReturnGitDiagnosisPlugin : IAIPlugin
{
[AIParameter("Short summary of the problem (1-2 sentences)", required: true)]
public string Problem { get; set; } = string.Empty;
[AIParameter("Explanation of why this happened", required: true)]
public string Cause { get; set; } = string.Empty;
[AIParameter("The exact git command(s) to fix the issue. Use newlines to separate multiple commands.", required: true)]
public string FixCommand { get; set; } = string.Empty;
[AIParameter("Warning about potential data loss or side effects. Leave empty if no warnings.", required: false)]
public string Warning { get; set; } = string.Empty;
[AIParameter("Risk level of the fix operation", required: true)]
public RiskLevel RiskLevel { get; set; } = RiskLevel.Low;
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["problem"] = typeof(string),
["cause"] = typeof(string),
["fixCommand"] = typeof(string),
["warning"] = typeof(string),
["riskLevel"] = typeof(RiskLevel)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new GitDiagnosisResult
{
Problem = Problem,
Cause = Cause,
FixCommand = FixCommand,
Warning = Warning ?? string.Empty,
RiskLevel = RiskLevel
};
return Task.FromResult(new AIPluginResult(result, "Git diagnosis returned"));
}
}

290
README.md Normal file
View File

@@ -0,0 +1,290 @@
# MarketAlly.GitCommitEditor
A production-ready .NET 9 library for analyzing, improving, and rewriting git commit messages using configurable quality rules and AI-powered suggestions.
## Features
- **Commit Message Analysis** - Score commit messages against configurable quality rules (subject length, conventional commits, banned phrases, etc.)
- **AI-Powered Suggestions** - Generate improved commit messages using Claude, OpenAI, Gemini, or Qwen
- **Repository Management** - Discover, register, and manage multiple git repositories
- **Commit Rewriting** - Amend latest commits or reword older commits in history with automatic backup
- **Batch Operations** - Process multiple commits across multiple repositories efficiently
- **History Health Analysis** - Comprehensive repository health scoring with cleanup recommendations
- **Push Operations** - Check push status, get tracking info, and perform force pushes
- **State Persistence** - Save and restore analysis state between sessions
- **Dependency Injection** - First-class support for Microsoft.Extensions.DependencyInjection
## Installation
```bash
dotnet add package MarketAlly.GitCommitEditor
```
## Quick Start
### With Dependency Injection (Recommended)
```csharp
using MarketAlly.GitCommitEditor.Extensions;
services.AddGitMessageImprover(options =>
{
options.WorkspaceRoot = @"C:\Projects";
options.StateFilePath = "git-state.json";
options.MaxCommitsPerRepo = 100;
options.AnalyzeSince = DateTimeOffset.Now.AddMonths(-3);
options.ExcludedAuthors = ["dependabot[bot]@users.noreply.github.com"];
// Configure commit message rules
options.Rules.MinSubjectLength = 10;
options.Rules.MaxSubjectLength = 72;
options.Rules.RequireConventionalCommit = true;
options.Rules.RequireIssueReference = false;
// Configure AI provider
options.Ai.Provider = "Claude";
options.Ai.Model = "claude-sonnet-4-20250514";
options.Ai.ApiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY")!;
options.Ai.IncludeDiffContext = true;
});
```
### Without DI (Simple Usage)
```csharp
using MarketAlly.GitCommitEditor.Options;
using MarketAlly.GitCommitEditor.Services;
var options = new GitImproverOptions
{
WorkspaceRoot = @"C:\Projects",
Ai = { ApiKey = "your-api-key", Provider = "Claude" }
};
await using var service = await GitMessageImproverService.CreateAsync(options);
```
## Core Workflows
### 1. Scan and Analyze Repositories
```csharp
IGitMessageImproverService improver = ...;
// Load persisted state
await improver.LoadStateAsync();
// Discover git repos in workspace
var newRepos = await improver.ScanAndRegisterReposAsync();
Console.WriteLine($"Found {newRepos.Count} new repositories");
// Analyze all repos for commit message issues
var analyses = await improver.AnalyzeAllReposAsync(
onlyNeedsImprovement: true,
progress: new Progress<(string Repo, int Count)>(p =>
Console.WriteLine($"Analyzed {p.Count} commits in {p.Repo}")));
Console.WriteLine($"Found {analyses.Count} commits needing improvement");
```
### 2. Generate AI Suggestions
```csharp
// Generate suggestions for commits that need improvement
var commitsToImprove = analyses.Where(a => a.Quality.NeedsImprovement).ToList();
var result = await improver.GenerateSuggestionsAsync(
commitsToImprove,
progress: new Progress<int>(count =>
Console.WriteLine($"Generated {count}/{commitsToImprove.Count} suggestions")));
Console.WriteLine($"Success: {result.SuccessCount}, Failed: {result.FailedCount}");
// Or generate for a single commit
var suggestion = await improver.GenerateSuggestionAsync(analyses.First());
if (suggestion.Success)
Console.WriteLine($"Suggested: {suggestion.Suggestion}");
```
### 3. Safe Batch Rewrite (Recommended)
```csharp
// Check safety before rewriting
var safetyInfo = improver.GetRewriteSafetyInfo(repoPath, commitsToRewrite);
if (!safetyInfo.CanProceedSafely)
{
foreach (var warning in safetyInfo.GetWarnings())
Console.WriteLine($"Warning: {warning}");
}
// Execute batch rewrite with automatic backup
var result = await improver.ExecuteBatchRewriteAsync(
repoPath,
commitsToRewrite,
createBackup: true,
progress: new Progress<(int Current, int Total, string Hash)>(p =>
Console.WriteLine($"Rewriting {p.Current}/{p.Total}: {p.Hash}")));
if (result.Success)
{
Console.WriteLine($"Rewrote {result.SuccessCount} commits");
if (result.BackupBranchName != null)
Console.WriteLine($"Backup branch: {result.BackupBranchName}");
if (result.RequiresForcePush)
Console.WriteLine("Force push required to update remote");
}
```
### 4. Preview and Apply Changes (Granular Control)
```csharp
// Preview changes before applying
var operations = improver.PreviewChanges(commitsToImprove);
foreach (var op in operations)
{
Console.WriteLine($"{op.CommitHash[..7]}: {op.OriginalMessage}");
Console.WriteLine($" -> {op.NewMessage}");
}
// Apply changes (dryRun: false to actually modify commits)
var result = await improver.ApplyChangesAsync(
operations,
dryRun: false,
progress: new Progress<(int Processed, int Total)>(p =>
Console.WriteLine($"Applied {p.Processed}/{p.Total}")));
Console.WriteLine($"Success: {result.Successful}, Failed: {result.Failed}");
```
### 5. Repository History Health Analysis
```csharp
// Analyze repository health
var healthReport = await improver.AnalyzeHistoryHealthAsync(
repoPath,
new HistoryAnalysisOptions { Depth = AnalysisDepth.Standard },
progress: new Progress<AnalysisProgress>(p =>
Console.WriteLine($"Analyzing: {p.PercentComplete}%")));
Console.WriteLine($"Health Score: {healthReport.Score.OverallScore}/100 ({healthReport.Score.Grade})");
Console.WriteLine($"Issues: {healthReport.CriticalIssueCount} critical, {healthReport.ErrorCount} errors");
// Export report
var markdown = await improver.ExportHealthReportAsync(healthReport, ReportFormat.Markdown);
// Execute cleanup suggestions
if (healthReport.CleanupSuggestions?.AutomatedOperations.Any() == true)
{
var cleanupResult = await improver.ExecuteAllCleanupsAsync(
repo,
healthReport.CleanupSuggestions,
new CleanupExecutionOptions { CreateBackup = true });
Console.WriteLine($"Cleaned up {cleanupResult.Successful} issues");
}
```
### 6. Handle Pushed Commits
```csharp
// Check if commit is already pushed
bool isPushed = improver.IsCommitPushed(repoPath, commitHash);
if (isPushed)
Console.WriteLine("Warning: Commit has been pushed. Force push required after amending.");
// Get tracking info
var tracking = improver.GetTrackingInfo(repoPath);
Console.WriteLine($"Tracking: {tracking.UpstreamBranch}, Ahead: {tracking.AheadBy}, Behind: {tracking.BehindBy}");
// After amending a pushed commit, force push
var pushResult = improver.ForcePush(repoPath);
if (pushResult.Success)
Console.WriteLine("Force push successful");
else
Console.WriteLine($"Push failed: {pushResult.Message}");
```
## Configuration
### GitImproverOptions
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `WorkspaceRoot` | string | required | Root directory to scan for git repositories |
| `StateFilePath` | string | `"git-improver-state.json"` | Path to persist state between sessions |
| `MaxCommitsPerRepo` | int | `100` | Maximum commits to analyze per repository |
| `AnalyzeSince` | DateTimeOffset? | `null` | Only analyze commits after this date |
| `ExcludedAuthors` | string[] | `[]` | Author emails to exclude (e.g., bots) |
| `Rules` | CommitMessageRules | default | Commit message quality rules |
| `Ai` | AiOptions | default | AI provider configuration |
### CommitMessageRules
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `MinSubjectLength` | int | `10` | Minimum subject line length |
| `MaxSubjectLength` | int | `72` | Maximum subject line length |
| `MinBodyLength` | int | `0` | Minimum body length (0 = optional) |
| `RequireConventionalCommit` | bool | `false` | Require conventional commit format |
| `RequireIssueReference` | bool | `false` | Require issue/ticket reference |
| `BannedPhrases` | string[] | see below | Phrases that trigger warnings |
| `ConventionalTypes` | string[] | see below | Valid conventional commit types |
**Default Banned Phrases:** `fix`, `wip`, `temp`, `asdf`, `test`, `stuff`, `things`, `changes`, `update`, `misc`, `minor`, `oops`, `commit`, `save`, `checkpoint`, `progress`, `done`, `finished`
**Default Conventional Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`
### AiOptions
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `ApiKey` | string | `""` | API key for the selected provider |
| `Provider` | string | `"Claude"` | AI provider (Claude, OpenAI, Gemini, Qwen) |
| `Model` | string | `"claude-sonnet-4-20250514"` | Model name for the provider |
| `IncludeDiffContext` | bool | `true` | Include diff in AI prompt |
| `MaxDiffLines` | int | `200` | Maximum diff lines to include |
| `MaxTokens` | int | `500` | Maximum tokens in AI response |
| `RateLimitDelayMs` | int | `500` | Delay between AI requests |
## Interfaces
The library follows Interface Segregation Principle with focused interfaces:
| Interface | Purpose |
|-----------|---------|
| `IRepositoryManager` | Repository discovery and registration |
| `ICommitAnalysisService` | Commit message analysis |
| `ISuggestionService` | AI suggestion generation |
| `ICommitRewriteService` | Commit message rewriting |
| `IGitPushService` | Push operations and tracking |
| `IHistoryHealthService` | Repository health analysis and cleanup |
| `IGitMessageImproverService` | Unified facade (composes all above) |
## Supported AI Providers
| Provider | Models |
|----------|--------|
| **Claude** | claude-sonnet-4-20250514, claude-opus-4-20250514, claude-3-5-haiku-20241022 |
| **OpenAI** | gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-3.5-turbo |
| **Gemini** | gemini-1.5-pro, gemini-1.5-flash, gemini-pro |
| **Qwen** | qwen-turbo, qwen-plus, qwen-max |
## Thread Safety
- `GitOperationsService` uses an LRU cache with TTL for repository handles
- State operations are async and support cancellation tokens
- The service implements `IDisposable` - ensure proper disposal
## Dependencies
- .NET 9.0
- MarketAlly.LibGit2Sharp (git operations)
- MarketAlly.AIPlugin (AI provider abstraction)
- Microsoft.Extensions.DependencyInjection.Abstractions
## License
MIT License - Copyright 2025 MarketAlly

1145
Resources/Strings/LibStrings.Designer.cs generated Executable file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,421 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="0" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- ==================== Commit Message Analyzer ==================== -->
<data name="Analyzer_MessageEmpty" xml:space="preserve">
<value>Commit-Nachricht ist leer</value>
</data>
<data name="Analyzer_SubjectTooShort" xml:space="preserve">
<value>Betreff hat {0} Zeichen, Minimum ist {1}</value>
</data>
<data name="Analyzer_SubjectTooLong" xml:space="preserve">
<value>Betreff hat {0} Zeichen, empfohlenes Maximum ist {1}</value>
</data>
<data name="Analyzer_BannedPhrase" xml:space="preserve">
<value>Betreff verwendet nicht-aussagekräftige Formulierung: '{0}'</value>
</data>
<data name="Analyzer_NotConventional" xml:space="preserve">
<value>Nachricht folgt nicht dem konventionellen Commit-Format (typ: betreff)</value>
</data>
<data name="Analyzer_UnknownType" xml:space="preserve">
<value>Unbekannter konventioneller Commit-Typ: {0}</value>
</data>
<data name="Analyzer_NoIssueRef" xml:space="preserve">
<value>Keine Issue-Referenz gefunden (z.B. #123 oder JIRA-123)</value>
</data>
<data name="Analyzer_CapitalLetter" xml:space="preserve">
<value>Betreff sollte mit einem Großbuchstaben beginnen</value>
</data>
<data name="Analyzer_NoPeriod" xml:space="preserve">
<value>Betreff sollte nicht mit einem Punkt enden</value>
</data>
<data name="Analyzer_ImperativeMood" xml:space="preserve">
<value>Verwenden Sie den Imperativ: '{0}' → '{1}' (z.B. 'Add' nicht 'Added')</value>
</data>
<data name="Analyzer_BodyTooShort" xml:space="preserve">
<value>Textkörper hat {0} Zeichen, Minimum ist {1}</value>
</data>
<data name="Analyzer_BlankLine" xml:space="preserve">
<value>Fügen Sie eine Leerzeile zwischen Betreff und Textkörper ein</value>
</data>
<data name="Analyzer_NotDescriptive" xml:space="preserve">
<value>'{0}' beschreibt nicht, was in {1} Dateien geändert wurde</value>
</data>
<data name="Analyzer_TooVague" xml:space="preserve">
<value>Nachricht ist zu vage für {0} geänderte Dateien - beschreiben Sie WAS sich geändert hat</value>
</data>
<data name="Analyzer_LargeChange" xml:space="preserve">
<value>Große Änderung ({0} Dateien, {1} Zeilen) verdient eine aussagekräftigere Nachricht</value>
</data>
<data name="Analyzer_MajorChange" xml:space="preserve">
<value>Größere Änderung ({0} Dateien) sollte einen Textkörper enthalten, der das Warum erklärt</value>
</data>
<data name="Analyzer_MentionArea" xml:space="preserve">
<value>Erwägen Sie zu erwähnen, welcher Bereich sich geändert hat (Dateien: {0})</value>
</data>
<!-- ==================== Git Operations Service ==================== -->
<data name="Git_NoCommits" xml:space="preserve">
<value>Keine Commits im Repository</value>
</data>
<data name="Git_CommitNotFound" xml:space="preserve">
<value>Commit nicht gefunden: {0}</value>
</data>
<data name="Git_NotAncestor" xml:space="preserve">
<value>Ziel-Commit ist kein Vorgänger von HEAD</value>
</data>
<data name="Git_NoTargetCommits" xml:space="preserve">
<value>Konnte keine Ziel-Commits im Repository finden</value>
</data>
<data name="Git_ParentMismatch" xml:space="preserve">
<value>Commit-Erstellung fehlgeschlagen: Eltern-Konflikt für Commit {0}</value>
</data>
<data name="Git_HeadUpdateFailed" xml:space="preserve">
<value>HEAD konnte nicht auf neuen Commit {0} aktualisiert werden</value>
</data>
<data name="Git_VerificationFailed" xml:space="preserve">
<value>Datenträger-Verifizierung fehlgeschlagen: HEAD sollte {0} sein, ist aber {1}</value>
</data>
<data name="Git_OldCommitReachable" xml:space="preserve">
<value>Alter Commit {0} ist nach dem Umschreiben immer noch von HEAD aus erreichbar</value>
</data>
<data name="Git_Error" xml:space="preserve">
<value>Git-Fehler: {0}</value>
</data>
<data name="Git_RemoteNotFound" xml:space="preserve">
<value>Remote '{0}' nicht gefunden</value>
</data>
<data name="Git_NoUpstreamNoOrigin" xml:space="preserve">
<value>Kein Upstream-Branch konfiguriert und kein 'origin' Remote gefunden.
Setzen Sie das Tracking manuell mit: git push -u origin {0}</value>
</data>
<data name="Git_ForcePushSuccess" xml:space="preserve">
<value>Force-Push erfolgreich</value>
</data>
<data name="Git_ForcePushedTo" xml:space="preserve">
<value>Force-Push zu origin/{0} erfolgreich</value>
</data>
<data name="Git_ProcessFailed" xml:space="preserve">
<value>Git-Prozess konnte nicht gestartet werden</value>
</data>
<data name="Git_ForcePushSuccessCmd" xml:space="preserve">
<value>Force-Push erfolgreich (über Git-Befehl)</value>
</data>
<data name="Git_PushFailed" xml:space="preserve">
<value>Push fehlgeschlagen: {0}</value>
</data>
<data name="Git_CommandFailed" xml:space="preserve">
<value>Git-Befehl konnte nicht ausgeführt werden: {0}</value>
</data>
<data name="Git_NoUpstream" xml:space="preserve">
<value>Kein Upstream-Branch konfiguriert. Setzen Sie das Tracking mit: git push -u origin &lt;branch&gt;</value>
</data>
<data name="Git_NonFastForward" xml:space="preserve">
<value>Push abgelehnt: non-fast-forward. Ziehen Sie zuerst Änderungen oder verwenden Sie Force-Push.</value>
</data>
<data name="Git_PushSuccessCmd" xml:space="preserve">
<value>Push erfolgreich (über Git-Befehl)</value>
</data>
<!-- ==================== Cleanup Executor ==================== -->
<data name="Cleanup_PushedCommitsBlocked" xml:space="preserve">
<value>Einige Commits wurden bereits gepusht. Aktivieren Sie 'AllowPushedCommits', um fortzufahren.</value>
</data>
<data name="Cleanup_NotImplemented" xml:space="preserve">
<value>Bereinigungstyp '{0}' ist noch nicht implementiert</value>
</data>
<data name="Cleanup_Rebuilding" xml:space="preserve">
<value>Commit-Historie wird neu aufgebaut...</value>
</data>
<data name="Cleanup_RebuildingCount" xml:space="preserve">
<value>{0} Commits werden neu aufgebaut...</value>
</data>
<data name="Cleanup_ProcessingCommit" xml:space="preserve">
<value>Commit {0}/{1} wird verarbeitet...</value>
</data>
<data name="Cleanup_UpdatingBranch" xml:space="preserve">
<value>Branch-Referenz wird aktualisiert...</value>
</data>
<data name="Cleanup_SquashingMerges" xml:space="preserve">
<value>Merge-Commits werden zusammengeführt...</value>
</data>
<data name="Cleanup_DropDuplicatesFailed" xml:space="preserve">
<value>Duplikat-Commits konnten nicht entfernt werden: {0}</value>
</data>
<data name="Cleanup_NeedTwoCommits" xml:space="preserve">
<value>Benötige mindestens 2 Commits zum Zusammenführen</value>
</data>
<data name="Cleanup_NoCommitsOnBranch" xml:space="preserve">
<value>Keine Commits auf dem aktuellen Branch gefunden</value>
</data>
<data name="Cleanup_NoMatchingCommits" xml:space="preserve">
<value>Keine passenden Commits zum Entfernen gefunden</value>
</data>
<data name="Cleanup_NoCommitsToSquash" xml:space="preserve">
<value>Keine Commits zum Zusammenführen angegeben</value>
</data>
<data name="Cleanup_NoMergeCommits" xml:space="preserve">
<value>Keine passenden Merge-Commits zum Zusammenführen gefunden</value>
</data>
<data name="Cleanup_SquashMergeFailed" xml:space="preserve">
<value>Merge-Commits konnten nicht zusammengeführt werden: {0}</value>
</data>
<data name="Cleanup_NoCommitsToFix" xml:space="preserve">
<value>Keine Commits zum Korrigieren angegeben</value>
</data>
<data name="Cleanup_FixAuthorFailed" xml:space="preserve">
<value>Autorenschaft konnte nicht korrigiert werden: {0}</value>
</data>
<data name="Cleanup_ConsolidatingFixes" xml:space="preserve">
<value>Merge-Fix-Commits werden konsolidiert...</value>
</data>
<data name="Cleanup_NoFixCommits" xml:space="preserve">
<value>Keine Fix-Commits zum Konsolidieren</value>
</data>
<data name="Cleanup_NoMatchingFixes" xml:space="preserve">
<value>Keine passenden Fix-Commits zum Konsolidieren gefunden</value>
</data>
<data name="Cleanup_ConsolidateFailed" xml:space="preserve">
<value>Merge-Fix-Commits konnten nicht konsolidiert werden: {0}</value>
</data>
<data name="Cleanup_ArchivingBranches" xml:space="preserve">
<value>Veraltete Branches werden archiviert...</value>
</data>
<data name="Cleanup_ProcessingBranch" xml:space="preserve">
<value>Branch {0} wird verarbeitet...</value>
</data>
<data name="Cleanup_ArchiveComplete" xml:space="preserve">
<value>Archivierung abgeschlossen</value>
</data>
<data name="Cleanup_ArchiveFailed" xml:space="preserve">
<value>Branches konnten nicht archiviert werden: {0}</value>
</data>
<data name="Cleanup_AnalyzingStructure" xml:space="preserve">
<value>Branch-Struktur wird analysiert...</value>
</data>
<data name="Cleanup_FoundCommits" xml:space="preserve">
<value>{0} Commits zum Linearisieren gefunden...</value>
</data>
<data name="Cleanup_Linearizing" xml:space="preserve">
<value>{0} Commits werden linearisiert ({1} Merges werden entfernt)...</value>
</data>
<data name="Cleanup_RebuildingCommit" xml:space="preserve">
<value>Commit {0}/{1} wird neu aufgebaut...</value>
</data>
<data name="Cleanup_Reconciling" xml:space="preserve">
<value>Endzustand wird abgeglichen...</value>
</data>
<data name="Cleanup_AlreadyLinear" xml:space="preserve">
<value>Historie ist bereits linear - keine Merge-Commits gefunden</value>
</data>
<data name="Cleanup_LinearizeComplete" xml:space="preserve">
<value>Linearisierung abgeschlossen</value>
</data>
<data name="Cleanup_LinearizeFailed" xml:space="preserve">
<value>Historie konnte nicht linearisiert werden: {0}</value>
</data>
<data name="Cleanup_DescReword" xml:space="preserve">
<value>Es werden {0} Commit-Nachricht(en) umformuliert, um die Qualität zu verbessern.</value>
</data>
<data name="Cleanup_DescSquash" xml:space="preserve">
<value>Es werden {0} doppelte Commits zu 1 zusammengeführt.</value>
</data>
<data name="Cleanup_DescConsolidate" xml:space="preserve">
<value>Es werden {0} Merge-Fix-Commits konsolidiert.</value>
</data>
<data name="Cleanup_DescAuthorship" xml:space="preserve">
<value>Autorenschaft wird bei {0} Commit(s) korrigiert.</value>
</data>
<data name="Cleanup_DescTrivialMerges" xml:space="preserve">
<value>Es werden {0} triviale Merges konsolidiert.</value>
</data>
<data name="Cleanup_DescArchive" xml:space="preserve">
<value>Veraltete Branches werden archiviert (löschen wenn gemergt, sonst taggen).</value>
</data>
<data name="Cleanup_DescLinearize" xml:space="preserve">
<value>Historie wird durch Entfernen von Merge-Commits und Sortierung nach Datum linearisiert.</value>
</data>
<data name="Cleanup_DescGeneric" xml:space="preserve">
<value>Es werden {0} Commit(s) verarbeitet.</value>
</data>
<data name="Cleanup_ReconcileMerge" xml:space="preserve">
<value>Abgleich: Endzustand nach Linearisierung mergen</value>
</data>
<!-- ==================== Validation ==================== -->
<data name="Validation_WorkspaceRequired" xml:space="preserve">
<value>WorkspaceRoot ist erforderlich</value>
</data>
<data name="Validation_WorkspaceNotFound" xml:space="preserve">
<value>WorkspaceRoot-Verzeichnis existiert nicht: {0}</value>
</data>
<data name="Validation_MaxCommitsPositive" xml:space="preserve">
<value>MaxCommitsPerRepo muss größer als 0 sein</value>
</data>
<data name="Validation_RulesNull" xml:space="preserve">
<value>Regeln dürfen nicht null sein</value>
</data>
<data name="Validation_AiOptionsNull" xml:space="preserve">
<value>KI-Optionen dürfen nicht null sein</value>
</data>
<data name="Validation_InvalidOptions" xml:space="preserve">
<value>Ungültige GitImproverOptions: {0}</value>
</data>
<data name="Validation_WeightsSum" xml:space="preserve">
<value>Gewichtungen müssen sich zu 1,0 summieren (aktuell: {0})</value>
</data>
<!-- ==================== Service Messages ==================== -->
<data name="Service_UnknownError" xml:space="preserve">
<value>Unbekannter Fehler</value>
</data>
<data name="Service_RepoNotRegistered" xml:space="preserve">
<value>Repository nicht registriert</value>
</data>
<data name="Service_UncommittedChanges" xml:space="preserve">
<value>Commits können nicht umgeschrieben werden, wenn es nicht committete Änderungen gibt. Bitte committen oder stashen Sie Ihre Änderungen zuerst.</value>
</data>
<data name="Service_RepoNotFound" xml:space="preserve">
<value>Repository nicht gefunden: {0}</value>
</data>
<data name="Service_NoSuggestion" xml:space="preserve">
<value>Keine vorgeschlagene Nachricht verfügbar</value>
</data>
<data name="Service_RepoNotRegisteredPath" xml:space="preserve">
<value>Repository nicht registriert: {0}</value>
</data>
<data name="Service_ApiKeyNotConfigured" xml:space="preserve">
<value>API-Schlüssel ist nicht konfiguriert. Bitte setzen Sie Ihren API-Schlüssel in den Einstellungen.</value>
</data>
<data name="Service_AiAnalysisFailed" xml:space="preserve">
<value>KI-Analyse fehlgeschlagen</value>
</data>
<data name="Service_AiFallback" xml:space="preserve">
<value>KI hat keine strukturierte Ausgabe zurückgegeben - auf ursprüngliche Nachricht zurückgefallen</value>
</data>
<data name="Service_PushSuccess" xml:space="preserve">
<value>Push erfolgreich</value>
</data>
<!-- ==================== Health Analyzer Status ==================== -->
<data name="Health_LoadingCommits" xml:space="preserve">
<value>Commits werden geladen</value>
</data>
<data name="Health_DetectingDuplicates" xml:space="preserve">
<value>Duplikate werden erkannt</value>
</data>
<data name="Health_AnalyzingMerges" xml:space="preserve">
<value>Merge-Commits werden analysiert</value>
</data>
<data name="Health_AnalyzingBranches" xml:space="preserve">
<value>Branch-Komplexität wird analysiert</value>
</data>
<data name="Health_AnalyzingMessages" xml:space="preserve">
<value>Nachrichtenqualität wird analysiert</value>
</data>
<data name="Health_AnalyzingAuthorship" xml:space="preserve">
<value>Autorenschaft wird analysiert</value>
</data>
<data name="Health_Complete" xml:space="preserve">
<value>Abgeschlossen</value>
</data>
<!-- ==================== Health Report Issues ==================== -->
<data name="Report_DuplicateContent" xml:space="preserve">
<value>Doppelte Commits mit identischem Inhalt</value>
</data>
<data name="Report_DuplicateContentDesc" xml:space="preserve">
<value>{0} Gruppen von Commits mit identischem Dateiinhalt gefunden ({1} redundante Commits). Diese können sicher zusammengeführt werden, da sie denselben Tree-SHA haben.</value>
</data>
<data name="Report_DuplicateMessages" xml:space="preserve">
<value>Commits mit doppelten Nachrichten</value>
</data>
<data name="Report_DuplicateMessagesDesc" xml:space="preserve">
<value>{0} Gruppen von Commits mit identischen Nachrichten aber unterschiedlichen Code-Änderungen gefunden ({1} Commits). Erwägen Sie aussagekräftigere Nachrichten zur Unterscheidung der Änderungen.</value>
</data>
<data name="Report_ExcessiveMerges" xml:space="preserve">
<value>Übermäßig viele Merge-Commits</value>
</data>
<data name="Report_HighMergeRatio" xml:space="preserve">
<value>Hoher Merge-Commit-Anteil</value>
</data>
<data name="Report_MergeRatioDesc" xml:space="preserve">
<value>Ihr Repository hat einen Merge-Commit-Anteil von {0}% ({1}/{2} Commits). Erwägen Sie einen Rebase-Workflow oder Squash-Merges.</value>
</data>
<data name="Report_MergeFixCommits" xml:space="preserve">
<value>Merge-Fix-Commits erkannt</value>
</data>
<data name="Report_MergeFixDesc" xml:space="preserve">
<value>{0} Commits mit Nachrichten wie 'fix merge' nach Merges erkannt.</value>
</data>
<data name="Report_CrossMerges" xml:space="preserve">
<value>Cross-Merges zwischen Branches</value>
</data>
<data name="Report_CrossMergesDesc" xml:space="preserve">
<value>{0} Cross-Merges zwischen Feature-Branches erkannt. Verwenden Sie Feature-Branches, die nur in main gemergt werden.</value>
</data>
<data name="Report_StaleBranches" xml:space="preserve">
<value>Veraltete Branches</value>
</data>
<data name="Report_StaleBranchesDesc" xml:space="preserve">
<value>{0} Branches ohne Aktivität in den letzten 30+ Tagen gefunden.</value>
</data>
<!-- ==================== Safety Warnings ==================== -->
<data name="Safety_UncommittedChanges" xml:space="preserve">
<value>Sie haben nicht committete Änderungen. Bitte committen oder stashen Sie diese zuerst.</value>
</data>
<data name="Safety_PushedCommits" xml:space="preserve">
<value>{0} Commit(s) wurden bereits auf das Remote gepusht. Das Umschreiben erfordert einen Force-Push und kann Mitarbeiter beeinträchtigen.</value>
</data>
<data name="Safety_BehindRemote" xml:space="preserve">
<value>Ihr Branch ist {0} Commit(s) hinter dem Remote zurück. Erwägen Sie zuerst zu pullen, um Konflikte zu vermeiden.</value>
</data>
<!-- ==================== Health Status ==================== -->
<data name="HealthStatus_NeedsAttention" xml:space="preserve">
<value>Repository hat erkennbare Probleme, die behoben werden sollten.</value>
</data>
<data name="HealthStatus_Critical" xml:space="preserve">
<value>Repository erfordert sofortige Aufmerksamkeit. Historie ist schwer beschädigt.</value>
</data>
</root>

View File

@@ -0,0 +1,421 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="0" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- ==================== Commit Message Analyzer ==================== -->
<data name="Analyzer_MessageEmpty" xml:space="preserve">
<value>El mensaje del commit está vacío</value>
</data>
<data name="Analyzer_SubjectTooShort" xml:space="preserve">
<value>El asunto tiene {0} caracteres, el mínimo es {1}</value>
</data>
<data name="Analyzer_SubjectTooLong" xml:space="preserve">
<value>El asunto tiene {0} caracteres, el máximo recomendado es {1}</value>
</data>
<data name="Analyzer_BannedPhrase" xml:space="preserve">
<value>El asunto usa una frase poco descriptiva: '{0}'</value>
</data>
<data name="Analyzer_NotConventional" xml:space="preserve">
<value>El mensaje no sigue el formato conventional commit (tipo: asunto)</value>
</data>
<data name="Analyzer_UnknownType" xml:space="preserve">
<value>Tipo de conventional commit desconocido: {0}</value>
</data>
<data name="Analyzer_NoIssueRef" xml:space="preserve">
<value>No se encontró referencia a issue (ej., #123 o JIRA-123)</value>
</data>
<data name="Analyzer_CapitalLetter" xml:space="preserve">
<value>El asunto debe comenzar con letra mayúscula</value>
</data>
<data name="Analyzer_NoPeriod" xml:space="preserve">
<value>El asunto no debe terminar con punto</value>
</data>
<data name="Analyzer_ImperativeMood" xml:space="preserve">
<value>Use modo imperativo: '{0}' → '{1}' (ej., 'Add' no 'Added')</value>
</data>
<data name="Analyzer_BodyTooShort" xml:space="preserve">
<value>El cuerpo tiene {0} caracteres, el mínimo es {1}</value>
</data>
<data name="Analyzer_BlankLine" xml:space="preserve">
<value>Agregue una línea en blanco entre el asunto y el cuerpo</value>
</data>
<data name="Analyzer_NotDescriptive" xml:space="preserve">
<value>'{0}' no describe lo que cambió en {1} archivos</value>
</data>
<data name="Analyzer_TooVague" xml:space="preserve">
<value>El mensaje es demasiado vago para {0} archivos modificados - describa QUÉ cambió</value>
</data>
<data name="Analyzer_LargeChange" xml:space="preserve">
<value>Cambio grande ({0} archivos, {1} líneas) merece un mensaje más descriptivo</value>
</data>
<data name="Analyzer_MajorChange" xml:space="preserve">
<value>Cambio importante ({0} archivos) debe incluir un cuerpo explicando por qué</value>
</data>
<data name="Analyzer_MentionArea" xml:space="preserve">
<value>Considere mencionar qué área cambió (archivos: {0})</value>
</data>
<!-- ==================== Git Operations Service ==================== -->
<data name="Git_NoCommits" xml:space="preserve">
<value>No hay commits en el repositorio</value>
</data>
<data name="Git_CommitNotFound" xml:space="preserve">
<value>Commit no encontrado: {0}</value>
</data>
<data name="Git_NotAncestor" xml:space="preserve">
<value>El commit objetivo no es un ancestro de HEAD</value>
</data>
<data name="Git_NoTargetCommits" xml:space="preserve">
<value>No se pudieron encontrar commits objetivo en el repositorio</value>
</data>
<data name="Git_ParentMismatch" xml:space="preserve">
<value>Falló la creación del commit: discrepancia de parent para el commit {0}</value>
</data>
<data name="Git_HeadUpdateFailed" xml:space="preserve">
<value>No se pudo actualizar HEAD al nuevo commit {0}</value>
</data>
<data name="Git_VerificationFailed" xml:space="preserve">
<value>Falló la verificación en disco: HEAD debería ser {0} pero es {1}</value>
</data>
<data name="Git_OldCommitReachable" xml:space="preserve">
<value>El commit antiguo {0} sigue siendo alcanzable desde HEAD después de la reescritura</value>
</data>
<data name="Git_Error" xml:space="preserve">
<value>Error de git: {0}</value>
</data>
<data name="Git_RemoteNotFound" xml:space="preserve">
<value>Remote '{0}' no encontrado</value>
</data>
<data name="Git_NoUpstreamNoOrigin" xml:space="preserve">
<value>No hay branch upstream configurado y no se encontró remote 'origin'.
Configure el seguimiento manualmente con: git push -u origin {0}</value>
</data>
<data name="Git_ForcePushSuccess" xml:space="preserve">
<value>Force push exitoso</value>
</data>
<data name="Git_ForcePushedTo" xml:space="preserve">
<value>Force push realizado a origin/{0}</value>
</data>
<data name="Git_ProcessFailed" xml:space="preserve">
<value>No se pudo iniciar el proceso de git</value>
</data>
<data name="Git_ForcePushSuccessCmd" xml:space="preserve">
<value>Force push exitoso (vía comando git)</value>
</data>
<data name="Git_PushFailed" xml:space="preserve">
<value>Push falló: {0}</value>
</data>
<data name="Git_CommandFailed" xml:space="preserve">
<value>No se pudo ejecutar el comando git: {0}</value>
</data>
<data name="Git_NoUpstream" xml:space="preserve">
<value>No hay branch upstream configurado. Configure el seguimiento con: git push -u origin &lt;branch&gt;</value>
</data>
<data name="Git_NonFastForward" xml:space="preserve">
<value>Push rechazado: non-fast-forward. Obtenga los cambios primero o use force push.</value>
</data>
<data name="Git_PushSuccessCmd" xml:space="preserve">
<value>Push exitoso (vía comando git)</value>
</data>
<!-- ==================== Cleanup Executor ==================== -->
<data name="Cleanup_PushedCommitsBlocked" xml:space="preserve">
<value>Algunos commits han sido pusheados. Habilite 'AllowPushedCommits' para continuar.</value>
</data>
<data name="Cleanup_NotImplemented" xml:space="preserve">
<value>El tipo de limpieza '{0}' aún no está implementado</value>
</data>
<data name="Cleanup_Rebuilding" xml:space="preserve">
<value>Reconstruyendo historial de commits...</value>
</data>
<data name="Cleanup_RebuildingCount" xml:space="preserve">
<value>Reconstruyendo {0} commits...</value>
</data>
<data name="Cleanup_ProcessingCommit" xml:space="preserve">
<value>Procesando commit {0}/{1}...</value>
</data>
<data name="Cleanup_UpdatingBranch" xml:space="preserve">
<value>Actualizando referencia del branch...</value>
</data>
<data name="Cleanup_SquashingMerges" xml:space="preserve">
<value>Realizando squash de merge commits...</value>
</data>
<data name="Cleanup_DropDuplicatesFailed" xml:space="preserve">
<value>No se pudieron eliminar commits duplicados: {0}</value>
</data>
<data name="Cleanup_NeedTwoCommits" xml:space="preserve">
<value>Se necesitan al menos 2 commits para hacer squash</value>
</data>
<data name="Cleanup_NoCommitsOnBranch" xml:space="preserve">
<value>No se encontraron commits en el branch actual</value>
</data>
<data name="Cleanup_NoMatchingCommits" xml:space="preserve">
<value>No se encontraron commits coincidentes para eliminar</value>
</data>
<data name="Cleanup_NoCommitsToSquash" xml:space="preserve">
<value>No se especificaron commits para hacer squash</value>
</data>
<data name="Cleanup_NoMergeCommits" xml:space="preserve">
<value>No se encontraron merge commits coincidentes para hacer squash</value>
</data>
<data name="Cleanup_SquashMergeFailed" xml:space="preserve">
<value>No se pudo hacer squash de merge commits: {0}</value>
</data>
<data name="Cleanup_NoCommitsToFix" xml:space="preserve">
<value>No se especificaron commits para corregir</value>
</data>
<data name="Cleanup_FixAuthorFailed" xml:space="preserve">
<value>No se pudo corregir la autoría: {0}</value>
</data>
<data name="Cleanup_ConsolidatingFixes" xml:space="preserve">
<value>Consolidando commits de corrección de merge...</value>
</data>
<data name="Cleanup_NoFixCommits" xml:space="preserve">
<value>No hay commits de corrección para consolidar</value>
</data>
<data name="Cleanup_NoMatchingFixes" xml:space="preserve">
<value>No se encontraron commits de corrección coincidentes para consolidar</value>
</data>
<data name="Cleanup_ConsolidateFailed" xml:space="preserve">
<value>No se pudieron consolidar commits de corrección de merge: {0}</value>
</data>
<data name="Cleanup_ArchivingBranches" xml:space="preserve">
<value>Archivando branches obsoletos...</value>
</data>
<data name="Cleanup_ProcessingBranch" xml:space="preserve">
<value>Procesando branch {0}...</value>
</data>
<data name="Cleanup_ArchiveComplete" xml:space="preserve">
<value>Archivo completado</value>
</data>
<data name="Cleanup_ArchiveFailed" xml:space="preserve">
<value>No se pudieron archivar branches: {0}</value>
</data>
<data name="Cleanup_AnalyzingStructure" xml:space="preserve">
<value>Analizando estructura del branch...</value>
</data>
<data name="Cleanup_FoundCommits" xml:space="preserve">
<value>Se encontraron {0} commits para linearizar...</value>
</data>
<data name="Cleanup_Linearizing" xml:space="preserve">
<value>Linearizando {0} commits (eliminando {1} merges)...</value>
</data>
<data name="Cleanup_RebuildingCommit" xml:space="preserve">
<value>Reconstruyendo commit {0}/{1}...</value>
</data>
<data name="Cleanup_Reconciling" xml:space="preserve">
<value>Reconciliando estado final...</value>
</data>
<data name="Cleanup_AlreadyLinear" xml:space="preserve">
<value>El historial ya es lineal - no se encontraron merge commits</value>
</data>
<data name="Cleanup_LinearizeComplete" xml:space="preserve">
<value>Linearización completada</value>
</data>
<data name="Cleanup_LinearizeFailed" xml:space="preserve">
<value>No se pudo linearizar el historial: {0}</value>
</data>
<data name="Cleanup_DescReword" xml:space="preserve">
<value>Se reescribirán {0} mensaje(s) de commit para mejorar la calidad.</value>
</data>
<data name="Cleanup_DescSquash" xml:space="preserve">
<value>Se hará squash de {0} commits duplicados en 1.</value>
</data>
<data name="Cleanup_DescConsolidate" xml:space="preserve">
<value>Se consolidarán {0} commits de corrección de merge.</value>
</data>
<data name="Cleanup_DescAuthorship" xml:space="preserve">
<value>Se corregirá la autoría en {0} commit(s).</value>
</data>
<data name="Cleanup_DescTrivialMerges" xml:space="preserve">
<value>Se consolidarán {0} merges triviales.</value>
</data>
<data name="Cleanup_DescArchive" xml:space="preserve">
<value>Se archivarán branches obsoletos (eliminar si están merged, etiquetar en caso contrario).</value>
</data>
<data name="Cleanup_DescLinearize" xml:space="preserve">
<value>Se linearizará el historial eliminando merge commits y ordenando por fecha.</value>
</data>
<data name="Cleanup_DescGeneric" xml:space="preserve">
<value>Se procesarán {0} commit(s).</value>
</data>
<data name="Cleanup_ReconcileMerge" xml:space="preserve">
<value>Reconciliar: merge del estado final después de la linearización</value>
</data>
<!-- ==================== Validation ==================== -->
<data name="Validation_WorkspaceRequired" xml:space="preserve">
<value>WorkspaceRoot es requerido</value>
</data>
<data name="Validation_WorkspaceNotFound" xml:space="preserve">
<value>El directorio WorkspaceRoot no existe: {0}</value>
</data>
<data name="Validation_MaxCommitsPositive" xml:space="preserve">
<value>MaxCommitsPerRepo debe ser mayor que 0</value>
</data>
<data name="Validation_RulesNull" xml:space="preserve">
<value>Rules no puede ser null</value>
</data>
<data name="Validation_AiOptionsNull" xml:space="preserve">
<value>Las opciones Ai no pueden ser null</value>
</data>
<data name="Validation_InvalidOptions" xml:space="preserve">
<value>GitImproverOptions inválidas: {0}</value>
</data>
<data name="Validation_WeightsSum" xml:space="preserve">
<value>Los pesos deben sumar 1.0 (actual: {0})</value>
</data>
<!-- ==================== Service Messages ==================== -->
<data name="Service_UnknownError" xml:space="preserve">
<value>Error desconocido</value>
</data>
<data name="Service_RepoNotRegistered" xml:space="preserve">
<value>Repositorio no registrado</value>
</data>
<data name="Service_UncommittedChanges" xml:space="preserve">
<value>No se pueden reescribir commits con cambios sin commitear. Por favor, haga commit o stash de sus cambios primero.</value>
</data>
<data name="Service_RepoNotFound" xml:space="preserve">
<value>Repositorio no encontrado: {0}</value>
</data>
<data name="Service_NoSuggestion" xml:space="preserve">
<value>No hay mensaje sugerido disponible</value>
</data>
<data name="Service_RepoNotRegisteredPath" xml:space="preserve">
<value>Repositorio no registrado: {0}</value>
</data>
<data name="Service_ApiKeyNotConfigured" xml:space="preserve">
<value>La clave API no está configurada. Por favor, configure su clave API en Configuración.</value>
</data>
<data name="Service_AiAnalysisFailed" xml:space="preserve">
<value>Falló el análisis de IA</value>
</data>
<data name="Service_AiFallback" xml:space="preserve">
<value>La IA no devolvió salida estructurada - se retornó al mensaje original</value>
</data>
<data name="Service_PushSuccess" xml:space="preserve">
<value>Push exitoso</value>
</data>
<!-- ==================== Health Analyzer Status ==================== -->
<data name="Health_LoadingCommits" xml:space="preserve">
<value>Cargando commits</value>
</data>
<data name="Health_DetectingDuplicates" xml:space="preserve">
<value>Detectando duplicados</value>
</data>
<data name="Health_AnalyzingMerges" xml:space="preserve">
<value>Analizando merge commits</value>
</data>
<data name="Health_AnalyzingBranches" xml:space="preserve">
<value>Analizando complejidad de branches</value>
</data>
<data name="Health_AnalyzingMessages" xml:space="preserve">
<value>Analizando calidad de mensajes</value>
</data>
<data name="Health_AnalyzingAuthorship" xml:space="preserve">
<value>Analizando autoría</value>
</data>
<data name="Health_Complete" xml:space="preserve">
<value>Completado</value>
</data>
<!-- ==================== Health Report Issues ==================== -->
<data name="Report_DuplicateContent" xml:space="preserve">
<value>Commits duplicados con contenido idéntico</value>
</data>
<data name="Report_DuplicateContentDesc" xml:space="preserve">
<value>Se encontraron {0} grupos de commits con contenido de archivo idéntico ({1} commits redundantes). Es seguro hacer squash ya que tienen el mismo tree SHA.</value>
</data>
<data name="Report_DuplicateMessages" xml:space="preserve">
<value>Commits con mensajes duplicados</value>
</data>
<data name="Report_DuplicateMessagesDesc" xml:space="preserve">
<value>Se encontraron {0} grupos de commits con mensajes idénticos pero cambios de código diferentes ({1} commits). Considere usar mensajes más descriptivos para diferenciar los cambios.</value>
</data>
<data name="Report_ExcessiveMerges" xml:space="preserve">
<value>Merge commits excesivos</value>
</data>
<data name="Report_HighMergeRatio" xml:space="preserve">
<value>Proporción alta de merge commits</value>
</data>
<data name="Report_MergeRatioDesc" xml:space="preserve">
<value>Su repositorio tiene una proporción de {0}% de merge commits ({1}/{2} commits). Considere usar flujo de trabajo de rebase o squash merges.</value>
</data>
<data name="Report_MergeFixCommits" xml:space="preserve">
<value>Commits de corrección de merge detectados</value>
</data>
<data name="Report_MergeFixDesc" xml:space="preserve">
<value>Se encontraron {0} commits con mensajes como 'fix merge' detectados después de merges.</value>
</data>
<data name="Report_CrossMerges" xml:space="preserve">
<value>Cross-merges entre branches</value>
</data>
<data name="Report_CrossMergesDesc" xml:space="preserve">
<value>Se detectaron {0} cross-merges entre feature branches. Use feature branches que solo hagan merge a main.</value>
</data>
<data name="Report_StaleBranches" xml:space="preserve">
<value>Branches obsoletos</value>
</data>
<data name="Report_StaleBranchesDesc" xml:space="preserve">
<value>Se encontraron {0} branches sin actividad en más de 30 días.</value>
</data>
<!-- ==================== Safety Warnings ==================== -->
<data name="Safety_UncommittedChanges" xml:space="preserve">
<value>Tiene cambios sin commitear. Por favor, haga commit o stash de ellos primero.</value>
</data>
<data name="Safety_PushedCommits" xml:space="preserve">
<value>{0} commit(s) ya han sido pusheados al remote. Reescribirlos requerirá un force push y puede afectar a colaboradores.</value>
</data>
<data name="Safety_BehindRemote" xml:space="preserve">
<value>Su branch está {0} commit(s) detrás del remote. Considere hacer pull primero para evitar conflictos.</value>
</data>
<!-- ==================== Health Status ==================== -->
<data name="HealthStatus_NeedsAttention" xml:space="preserve">
<value>El repositorio tiene problemas notables que deben abordarse.</value>
</data>
<data name="HealthStatus_Critical" xml:space="preserve">
<value>El repositorio requiere atención inmediata. El historial está severamente degradado.</value>
</data>
</root>

View File

@@ -0,0 +1,421 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="0" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- ==================== Commit Message Analyzer ==================== -->
<data name="Analyzer_MessageEmpty" xml:space="preserve">
<value>Le message de commit est vide</value>
</data>
<data name="Analyzer_SubjectTooShort" xml:space="preserve">
<value>Le sujet contient {0} caractères, le minimum est {1}</value>
</data>
<data name="Analyzer_SubjectTooLong" xml:space="preserve">
<value>Le sujet contient {0} caractères, le maximum recommandé est {1}</value>
</data>
<data name="Analyzer_BannedPhrase" xml:space="preserve">
<value>Le sujet utilise une expression non descriptive : '{0}'</value>
</data>
<data name="Analyzer_NotConventional" xml:space="preserve">
<value>Le message ne suit pas le format de commit conventionnel (type: sujet)</value>
</data>
<data name="Analyzer_UnknownType" xml:space="preserve">
<value>Type de commit conventionnel inconnu : {0}</value>
</data>
<data name="Analyzer_NoIssueRef" xml:space="preserve">
<value>Aucune référence de ticket trouvée (par exemple, #123 ou JIRA-123)</value>
</data>
<data name="Analyzer_CapitalLetter" xml:space="preserve">
<value>Le sujet devrait commencer par une majuscule</value>
</data>
<data name="Analyzer_NoPeriod" xml:space="preserve">
<value>Le sujet ne devrait pas se terminer par un point</value>
</data>
<data name="Analyzer_ImperativeMood" xml:space="preserve">
<value>Utilisez le mode impératif : '{0}' → '{1}' (par exemple, 'Ajoute' et non 'Ajouté')</value>
</data>
<data name="Analyzer_BodyTooShort" xml:space="preserve">
<value>Le corps contient {0} caractères, le minimum est {1}</value>
</data>
<data name="Analyzer_BlankLine" xml:space="preserve">
<value>Ajoutez une ligne vide entre le sujet et le corps</value>
</data>
<data name="Analyzer_NotDescriptive" xml:space="preserve">
<value>'{0}' ne décrit pas ce qui a changé dans {1} fichiers</value>
</data>
<data name="Analyzer_TooVague" xml:space="preserve">
<value>Le message est trop vague pour {0} fichiers modifiés - décrivez CE QUI a changé</value>
</data>
<data name="Analyzer_LargeChange" xml:space="preserve">
<value>Un changement important ({0} fichiers, {1} lignes) mérite un message plus descriptif</value>
</data>
<data name="Analyzer_MajorChange" xml:space="preserve">
<value>Un changement majeur ({0} fichiers) devrait inclure un corps expliquant pourquoi</value>
</data>
<data name="Analyzer_MentionArea" xml:space="preserve">
<value>Envisagez de mentionner la zone modifiée (fichiers : {0})</value>
</data>
<!-- ==================== Git Operations Service ==================== -->
<data name="Git_NoCommits" xml:space="preserve">
<value>Aucun commit dans le dépôt</value>
</data>
<data name="Git_CommitNotFound" xml:space="preserve">
<value>Commit introuvable : {0}</value>
</data>
<data name="Git_NotAncestor" xml:space="preserve">
<value>Le commit cible n'est pas un ancêtre de HEAD</value>
</data>
<data name="Git_NoTargetCommits" xml:space="preserve">
<value>Impossible de trouver des commits cibles dans le dépôt</value>
</data>
<data name="Git_ParentMismatch" xml:space="preserve">
<value>Échec de la création du commit : incohérence de parent pour le commit {0}</value>
</data>
<data name="Git_HeadUpdateFailed" xml:space="preserve">
<value>Échec de la mise à jour de HEAD vers le nouveau commit {0}</value>
</data>
<data name="Git_VerificationFailed" xml:space="preserve">
<value>Échec de la vérification du disque : HEAD devrait être {0} mais est {1}</value>
</data>
<data name="Git_OldCommitReachable" xml:space="preserve">
<value>L'ancien commit {0} est toujours accessible depuis HEAD après la réécriture</value>
</data>
<data name="Git_Error" xml:space="preserve">
<value>Erreur Git : {0}</value>
</data>
<data name="Git_RemoteNotFound" xml:space="preserve">
<value>Dépôt distant '{0}' introuvable</value>
</data>
<data name="Git_NoUpstreamNoOrigin" xml:space="preserve">
<value>Aucune branche amont configurée et aucun dépôt distant 'origin' trouvé.
Définissez le suivi manuellement avec : git push -u origin {0}</value>
</data>
<data name="Git_ForcePushSuccess" xml:space="preserve">
<value>Push forcé réussi</value>
</data>
<data name="Git_ForcePushedTo" xml:space="preserve">
<value>Push forcé vers origin/{0}</value>
</data>
<data name="Git_ProcessFailed" xml:space="preserve">
<value>Échec du démarrage du processus git</value>
</data>
<data name="Git_ForcePushSuccessCmd" xml:space="preserve">
<value>Push forcé réussi (via commande git)</value>
</data>
<data name="Git_PushFailed" xml:space="preserve">
<value>Échec du push : {0}</value>
</data>
<data name="Git_CommandFailed" xml:space="preserve">
<value>Échec de l'exécution de la commande git : {0}</value>
</data>
<data name="Git_NoUpstream" xml:space="preserve">
<value>Aucune branche amont configurée. Définissez le suivi avec : git push -u origin &lt;branch&gt;</value>
</data>
<data name="Git_NonFastForward" xml:space="preserve">
<value>Push rejeté : non-fast-forward. Récupérez d'abord les modifications ou utilisez le push forcé.</value>
</data>
<data name="Git_PushSuccessCmd" xml:space="preserve">
<value>Push réussi (via commande git)</value>
</data>
<!-- ==================== Cleanup Executor ==================== -->
<data name="Cleanup_PushedCommitsBlocked" xml:space="preserve">
<value>Certains commits ont été poussés. Activez 'AllowPushedCommits' pour continuer.</value>
</data>
<data name="Cleanup_NotImplemented" xml:space="preserve">
<value>Le type de nettoyage '{0}' n'est pas encore implémenté</value>
</data>
<data name="Cleanup_Rebuilding" xml:space="preserve">
<value>Reconstruction de l'historique des commits...</value>
</data>
<data name="Cleanup_RebuildingCount" xml:space="preserve">
<value>Reconstruction de {0} commits...</value>
</data>
<data name="Cleanup_ProcessingCommit" xml:space="preserve">
<value>Traitement du commit {0}/{1}...</value>
</data>
<data name="Cleanup_UpdatingBranch" xml:space="preserve">
<value>Mise à jour de la référence de branche...</value>
</data>
<data name="Cleanup_SquashingMerges" xml:space="preserve">
<value>Écrasement des commits de merge...</value>
</data>
<data name="Cleanup_DropDuplicatesFailed" xml:space="preserve">
<value>Échec de la suppression des commits en double : {0}</value>
</data>
<data name="Cleanup_NeedTwoCommits" xml:space="preserve">
<value>Au moins 2 commits sont nécessaires pour écraser</value>
</data>
<data name="Cleanup_NoCommitsOnBranch" xml:space="preserve">
<value>Aucun commit trouvé sur la branche actuelle</value>
</data>
<data name="Cleanup_NoMatchingCommits" xml:space="preserve">
<value>Aucun commit correspondant trouvé à supprimer</value>
</data>
<data name="Cleanup_NoCommitsToSquash" xml:space="preserve">
<value>Aucun commit spécifié à écraser</value>
</data>
<data name="Cleanup_NoMergeCommits" xml:space="preserve">
<value>Aucun commit de merge correspondant trouvé à écraser</value>
</data>
<data name="Cleanup_SquashMergeFailed" xml:space="preserve">
<value>Échec de l'écrasement des commits de merge : {0}</value>
</data>
<data name="Cleanup_NoCommitsToFix" xml:space="preserve">
<value>Aucun commit spécifié à corriger</value>
</data>
<data name="Cleanup_FixAuthorFailed" xml:space="preserve">
<value>Échec de la correction de la paternité : {0}</value>
</data>
<data name="Cleanup_ConsolidatingFixes" xml:space="preserve">
<value>Consolidation des commits de correction de merge...</value>
</data>
<data name="Cleanup_NoFixCommits" xml:space="preserve">
<value>Aucun commit de correction à consolider</value>
</data>
<data name="Cleanup_NoMatchingFixes" xml:space="preserve">
<value>Aucun commit de correction correspondant trouvé à consolider</value>
</data>
<data name="Cleanup_ConsolidateFailed" xml:space="preserve">
<value>Échec de la consolidation des commits de correction de merge : {0}</value>
</data>
<data name="Cleanup_ArchivingBranches" xml:space="preserve">
<value>Archivage des branches obsolètes...</value>
</data>
<data name="Cleanup_ProcessingBranch" xml:space="preserve">
<value>Traitement de la branche {0}...</value>
</data>
<data name="Cleanup_ArchiveComplete" xml:space="preserve">
<value>Archivage terminé</value>
</data>
<data name="Cleanup_ArchiveFailed" xml:space="preserve">
<value>Échec de l'archivage des branches : {0}</value>
</data>
<data name="Cleanup_AnalyzingStructure" xml:space="preserve">
<value>Analyse de la structure des branches...</value>
</data>
<data name="Cleanup_FoundCommits" xml:space="preserve">
<value>{0} commits trouvés à linéariser...</value>
</data>
<data name="Cleanup_Linearizing" xml:space="preserve">
<value>Linéarisation de {0} commits (suppression de {1} merges)...</value>
</data>
<data name="Cleanup_RebuildingCommit" xml:space="preserve">
<value>Reconstruction du commit {0}/{1}...</value>
</data>
<data name="Cleanup_Reconciling" xml:space="preserve">
<value>Réconciliation de l'état final...</value>
</data>
<data name="Cleanup_AlreadyLinear" xml:space="preserve">
<value>L'historique est déjà linéaire - aucun commit de merge trouvé</value>
</data>
<data name="Cleanup_LinearizeComplete" xml:space="preserve">
<value>Linéarisation terminée</value>
</data>
<data name="Cleanup_LinearizeFailed" xml:space="preserve">
<value>Échec de la linéarisation de l'historique : {0}</value>
</data>
<data name="Cleanup_DescReword" xml:space="preserve">
<value>Reformulera {0} message(s) de commit pour améliorer la qualité.</value>
</data>
<data name="Cleanup_DescSquash" xml:space="preserve">
<value>Écrasera {0} commits en double en 1.</value>
</data>
<data name="Cleanup_DescConsolidate" xml:space="preserve">
<value>Consolidera {0} commits de correction de merge.</value>
</data>
<data name="Cleanup_DescAuthorship" xml:space="preserve">
<value>Corrigera la paternité sur {0} commit(s).</value>
</data>
<data name="Cleanup_DescTrivialMerges" xml:space="preserve">
<value>Consolidera {0} merges triviaux.</value>
</data>
<data name="Cleanup_DescArchive" xml:space="preserve">
<value>Archivera les branches obsolètes (suppression si fusionnées, étiquetage sinon).</value>
</data>
<data name="Cleanup_DescLinearize" xml:space="preserve">
<value>Linéarisera l'historique en supprimant les commits de merge et en triant par date.</value>
</data>
<data name="Cleanup_DescGeneric" xml:space="preserve">
<value>Traitera {0} commit(s).</value>
</data>
<data name="Cleanup_ReconcileMerge" xml:space="preserve">
<value>Réconciliation : fusion de l'état final après linéarisation</value>
</data>
<!-- ==================== Validation ==================== -->
<data name="Validation_WorkspaceRequired" xml:space="preserve">
<value>WorkspaceRoot est requis</value>
</data>
<data name="Validation_WorkspaceNotFound" xml:space="preserve">
<value>Le répertoire WorkspaceRoot n'existe pas : {0}</value>
</data>
<data name="Validation_MaxCommitsPositive" xml:space="preserve">
<value>MaxCommitsPerRepo doit être supérieur à 0</value>
</data>
<data name="Validation_RulesNull" xml:space="preserve">
<value>Rules ne peut pas être null</value>
</data>
<data name="Validation_AiOptionsNull" xml:space="preserve">
<value>Les options Ai ne peuvent pas être null</value>
</data>
<data name="Validation_InvalidOptions" xml:space="preserve">
<value>GitImproverOptions invalides : {0}</value>
</data>
<data name="Validation_WeightsSum" xml:space="preserve">
<value>Les poids doivent totaliser 1.0 (actuel : {0})</value>
</data>
<!-- ==================== Service Messages ==================== -->
<data name="Service_UnknownError" xml:space="preserve">
<value>Erreur inconnue</value>
</data>
<data name="Service_RepoNotRegistered" xml:space="preserve">
<value>Dépôt non enregistré</value>
</data>
<data name="Service_UncommittedChanges" xml:space="preserve">
<value>Impossible de réécrire les commits avec des modifications non validées. Veuillez d'abord valider ou mettre en réserve vos modifications.</value>
</data>
<data name="Service_RepoNotFound" xml:space="preserve">
<value>Dépôt introuvable : {0}</value>
</data>
<data name="Service_NoSuggestion" xml:space="preserve">
<value>Aucun message suggéré disponible</value>
</data>
<data name="Service_RepoNotRegisteredPath" xml:space="preserve">
<value>Dépôt non enregistré : {0}</value>
</data>
<data name="Service_ApiKeyNotConfigured" xml:space="preserve">
<value>La clé API n'est pas configurée. Veuillez définir votre clé API dans les Paramètres.</value>
</data>
<data name="Service_AiAnalysisFailed" xml:space="preserve">
<value>Échec de l'analyse IA</value>
</data>
<data name="Service_AiFallback" xml:space="preserve">
<value>L'IA n'a pas retourné de sortie structurée - retour au message original</value>
</data>
<data name="Service_PushSuccess" xml:space="preserve">
<value>Push réussi</value>
</data>
<!-- ==================== Health Analyzer Status ==================== -->
<data name="Health_LoadingCommits" xml:space="preserve">
<value>Chargement des commits</value>
</data>
<data name="Health_DetectingDuplicates" xml:space="preserve">
<value>Détection des doublons</value>
</data>
<data name="Health_AnalyzingMerges" xml:space="preserve">
<value>Analyse des commits de merge</value>
</data>
<data name="Health_AnalyzingBranches" xml:space="preserve">
<value>Analyse de la complexité des branches</value>
</data>
<data name="Health_AnalyzingMessages" xml:space="preserve">
<value>Analyse de la qualité des messages</value>
</data>
<data name="Health_AnalyzingAuthorship" xml:space="preserve">
<value>Analyse de la paternité</value>
</data>
<data name="Health_Complete" xml:space="preserve">
<value>Terminé</value>
</data>
<!-- ==================== Health Report Issues ==================== -->
<data name="Report_DuplicateContent" xml:space="preserve">
<value>Commits en double avec un contenu identique</value>
</data>
<data name="Report_DuplicateContentDesc" xml:space="preserve">
<value>{0} groupes de commits avec un contenu de fichier identique trouvés ({1} commits redondants). Ceux-ci peuvent être écrasés en toute sécurité car ils ont le même SHA d'arborescence.</value>
</data>
<data name="Report_DuplicateMessages" xml:space="preserve">
<value>Commits avec des messages en double</value>
</data>
<data name="Report_DuplicateMessagesDesc" xml:space="preserve">
<value>{0} groupes de commits avec des messages identiques mais des modifications de code différentes trouvés ({1} commits). Envisagez d'utiliser des messages plus descriptifs pour différencier les changements.</value>
</data>
<data name="Report_ExcessiveMerges" xml:space="preserve">
<value>Commits de merge excessifs</value>
</data>
<data name="Report_HighMergeRatio" xml:space="preserve">
<value>Ratio élevé de commits de merge</value>
</data>
<data name="Report_MergeRatioDesc" xml:space="preserve">
<value>Votre dépôt a un ratio de commits de merge de {0}% ({1}/{2} commits). Envisagez d'utiliser le flux de travail rebase ou les merges écrasés.</value>
</data>
<data name="Report_MergeFixCommits" xml:space="preserve">
<value>Commits de correction de merge détectés</value>
</data>
<data name="Report_MergeFixDesc" xml:space="preserve">
<value>{0} commits avec des messages du type 'fix merge' détectés après des merges.</value>
</data>
<data name="Report_CrossMerges" xml:space="preserve">
<value>Cross-merges entre branches</value>
</data>
<data name="Report_CrossMergesDesc" xml:space="preserve">
<value>{0} cross-merges entre branches de fonctionnalité détectés. Utilisez des branches de fonctionnalité qui fusionnent uniquement dans main.</value>
</data>
<data name="Report_StaleBranches" xml:space="preserve">
<value>Branches obsolètes</value>
</data>
<data name="Report_StaleBranchesDesc" xml:space="preserve">
<value>{0} branches sans activité depuis plus de 30 jours trouvées.</value>
</data>
<!-- ==================== Safety Warnings ==================== -->
<data name="Safety_UncommittedChanges" xml:space="preserve">
<value>Vous avez des modifications non validées. Veuillez d'abord les valider ou les mettre en réserve.</value>
</data>
<data name="Safety_PushedCommits" xml:space="preserve">
<value>{0} commit(s) ont déjà été poussés vers le dépôt distant. Les réécrire nécessitera un push forcé et pourrait affecter les collaborateurs.</value>
</data>
<data name="Safety_BehindRemote" xml:space="preserve">
<value>Votre branche est en retard de {0} commit(s) par rapport au dépôt distant. Envisagez de faire un pull d'abord pour éviter les conflits.</value>
</data>
<!-- ==================== Health Status ==================== -->
<data name="HealthStatus_NeedsAttention" xml:space="preserve">
<value>Le dépôt présente des problèmes notables qui devraient être traités.</value>
</data>
<data name="HealthStatus_Critical" xml:space="preserve">
<value>Le dépôt nécessite une attention immédiate. L'historique est gravement dégradé.</value>
</data>
</root>

View File

@@ -0,0 +1,421 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="0" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- ==================== Commit Message Analyzer ==================== -->
<data name="Analyzer_MessageEmpty" xml:space="preserve">
<value>Commit संदेश खाली है</value>
</data>
<data name="Analyzer_SubjectTooShort" xml:space="preserve">
<value>विषय {0} वर्णों का है, न्यूनतम {1} होना चाहिए</value>
</data>
<data name="Analyzer_SubjectTooLong" xml:space="preserve">
<value>विषय {0} वर्णों का है, अनुशंसित अधिकतम {1} है</value>
</data>
<data name="Analyzer_BannedPhrase" xml:space="preserve">
<value>विषय में गैर-वर्णनात्मक वाक्यांश का उपयोग: '{0}'</value>
</data>
<data name="Analyzer_NotConventional" xml:space="preserve">
<value>संदेश conventional commit प्रारूप का पालन नहीं करता (type: subject)</value>
</data>
<data name="Analyzer_UnknownType" xml:space="preserve">
<value>अज्ञात conventional commit प्रकार: {0}</value>
</data>
<data name="Analyzer_NoIssueRef" xml:space="preserve">
<value>कोई issue संदर्भ नहीं मिला (जैसे, #123 या JIRA-123)</value>
</data>
<data name="Analyzer_CapitalLetter" xml:space="preserve">
<value>विषय बड़े अक्षर से शुरू होना चाहिए</value>
</data>
<data name="Analyzer_NoPeriod" xml:space="preserve">
<value>विषय पूर्ण विराम के साथ समाप्त नहीं होना चाहिए</value>
</data>
<data name="Analyzer_ImperativeMood" xml:space="preserve">
<value>आज्ञार्थक मूड का उपयोग करें: '{0}' → '{1}' (जैसे, 'Add' न कि 'Added')</value>
</data>
<data name="Analyzer_BodyTooShort" xml:space="preserve">
<value>मुख्य भाग {0} वर्णों का है, न्यूनतम {1} होना चाहिए</value>
</data>
<data name="Analyzer_BlankLine" xml:space="preserve">
<value>विषय और मुख्य भाग के बीच एक रिक्त पंक्ति जोड़ें</value>
</data>
<data name="Analyzer_NotDescriptive" xml:space="preserve">
<value>'{0}' यह वर्णन नहीं करता कि {1} फ़ाइलों में क्या बदला</value>
</data>
<data name="Analyzer_TooVague" xml:space="preserve">
<value>संदेश {0} बदली गई फ़ाइलों के लिए बहुत अस्पष्ट है - वर्णन करें कि क्या बदला</value>
</data>
<data name="Analyzer_LargeChange" xml:space="preserve">
<value>बड़े परिवर्तन ({0} फ़ाइलें, {1} पंक्तियाँ) के लिए अधिक विस्तृत संदेश की आवश्यकता है</value>
</data>
<data name="Analyzer_MajorChange" xml:space="preserve">
<value>प्रमुख परिवर्तन ({0} फ़ाइलें) में मुख्य भाग शामिल होना चाहिए जो बताए कि क्यों</value>
</data>
<data name="Analyzer_MentionArea" xml:space="preserve">
<value>किस क्षेत्र में परिवर्तन हुआ इसका उल्लेख करने पर विचार करें (फ़ाइलें: {0})</value>
</data>
<!-- ==================== Git Operations Service ==================== -->
<data name="Git_NoCommits" xml:space="preserve">
<value>Repository में कोई commit नहीं</value>
</data>
<data name="Git_CommitNotFound" xml:space="preserve">
<value>Commit नहीं मिला: {0}</value>
</data>
<data name="Git_NotAncestor" xml:space="preserve">
<value>लक्ष्य commit HEAD का पूर्वज नहीं है</value>
</data>
<data name="Git_NoTargetCommits" xml:space="preserve">
<value>Repository में कोई लक्ष्य commit नहीं मिला</value>
</data>
<data name="Git_ParentMismatch" xml:space="preserve">
<value>Commit निर्माण विफल: commit {0} के लिए parent बेमेल</value>
</data>
<data name="Git_HeadUpdateFailed" xml:space="preserve">
<value>नए commit {0} में HEAD अपडेट करना विफल</value>
</data>
<data name="Git_VerificationFailed" xml:space="preserve">
<value>डिस्क सत्यापन विफल: HEAD {0} होना चाहिए लेकिन {1} है</value>
</data>
<data name="Git_OldCommitReachable" xml:space="preserve">
<value>पुराना commit {0} rewrite के बाद भी HEAD से पहुंचने योग्य है</value>
</data>
<data name="Git_Error" xml:space="preserve">
<value>Git त्रुटि: {0}</value>
</data>
<data name="Git_RemoteNotFound" xml:space="preserve">
<value>Remote '{0}' नहीं मिला</value>
</data>
<data name="Git_NoUpstreamNoOrigin" xml:space="preserve">
<value>कोई upstream branch कॉन्फ़िगर नहीं है और कोई 'origin' remote नहीं मिला।
इसके साथ मैन्युअल रूप से tracking सेट करें: git push -u origin {0}</value>
</data>
<data name="Git_ForcePushSuccess" xml:space="preserve">
<value>Force push सफल</value>
</data>
<data name="Git_ForcePushedTo" xml:space="preserve">
<value>origin/{0} में force push किया गया</value>
</data>
<data name="Git_ProcessFailed" xml:space="preserve">
<value>Git प्रक्रिया शुरू करना विफल</value>
</data>
<data name="Git_ForcePushSuccessCmd" xml:space="preserve">
<value>Force push सफल (git command के माध्यम से)</value>
</data>
<data name="Git_PushFailed" xml:space="preserve">
<value>Push विफल: {0}</value>
</data>
<data name="Git_CommandFailed" xml:space="preserve">
<value>Git command चलाना विफल: {0}</value>
</data>
<data name="Git_NoUpstream" xml:space="preserve">
<value>कोई upstream branch कॉन्फ़िगर नहीं है। इसके साथ tracking सेट करें: git push -u origin &lt;branch&gt;</value>
</data>
<data name="Git_NonFastForward" xml:space="preserve">
<value>Push अस्वीकृत: non-fast-forward। पहले परिवर्तन pull करें या force push का उपयोग करें।</value>
</data>
<data name="Git_PushSuccessCmd" xml:space="preserve">
<value>Push सफल (git command के माध्यम से)</value>
</data>
<!-- ==================== Cleanup Executor ==================== -->
<data name="Cleanup_PushedCommitsBlocked" xml:space="preserve">
<value>कुछ commits push किए जा चुके हैं। आगे बढ़ने के लिए 'AllowPushedCommits' सक्षम करें।</value>
</data>
<data name="Cleanup_NotImplemented" xml:space="preserve">
<value>Cleanup प्रकार '{0}' अभी तक लागू नहीं किया गया है</value>
</data>
<data name="Cleanup_Rebuilding" xml:space="preserve">
<value>Commit इतिहास का पुनर्निर्माण हो रहा है...</value>
</data>
<data name="Cleanup_RebuildingCount" xml:space="preserve">
<value>{0} commits का पुनर्निर्माण हो रहा है...</value>
</data>
<data name="Cleanup_ProcessingCommit" xml:space="preserve">
<value>Commit {0}/{1} प्रोसेस हो रहा है...</value>
</data>
<data name="Cleanup_UpdatingBranch" xml:space="preserve">
<value>Branch संदर्भ अपडेट हो रहा है...</value>
</data>
<data name="Cleanup_SquashingMerges" xml:space="preserve">
<value>Merge commits squash हो रहे हैं...</value>
</data>
<data name="Cleanup_DropDuplicatesFailed" xml:space="preserve">
<value>डुप्लिकेट commits हटाना विफल: {0}</value>
</data>
<data name="Cleanup_NeedTwoCommits" xml:space="preserve">
<value>Squash करने के लिए कम से कम 2 commits की आवश्यकता है</value>
</data>
<data name="Cleanup_NoCommitsOnBranch" xml:space="preserve">
<value>वर्तमान branch पर कोई commit नहीं मिला</value>
</data>
<data name="Cleanup_NoMatchingCommits" xml:space="preserve">
<value>हटाने के लिए कोई मिलान करने वाले commits नहीं मिले</value>
</data>
<data name="Cleanup_NoCommitsToSquash" xml:space="preserve">
<value>Squash करने के लिए कोई commits निर्दिष्ट नहीं</value>
</data>
<data name="Cleanup_NoMergeCommits" xml:space="preserve">
<value>Squash करने के लिए कोई मिलान करने वाले merge commits नहीं मिले</value>
</data>
<data name="Cleanup_SquashMergeFailed" xml:space="preserve">
<value>Merge commits squash करना विफल: {0}</value>
</data>
<data name="Cleanup_NoCommitsToFix" xml:space="preserve">
<value>ठीक करने के लिए कोई commits निर्दिष्ट नहीं</value>
</data>
<data name="Cleanup_FixAuthorFailed" xml:space="preserve">
<value>Authorship ठीक करना विफल: {0}</value>
</data>
<data name="Cleanup_ConsolidatingFixes" xml:space="preserve">
<value>Merge fix commits समेकित हो रहे हैं...</value>
</data>
<data name="Cleanup_NoFixCommits" xml:space="preserve">
<value>समेकित करने के लिए कोई fix commits नहीं</value>
</data>
<data name="Cleanup_NoMatchingFixes" xml:space="preserve">
<value>समेकित करने के लिए कोई मिलान करने वाले fix commits नहीं मिले</value>
</data>
<data name="Cleanup_ConsolidateFailed" xml:space="preserve">
<value>Merge fix commits समेकित करना विफल: {0}</value>
</data>
<data name="Cleanup_ArchivingBranches" xml:space="preserve">
<value>पुरानी branches संग्रहीत हो रही हैं...</value>
</data>
<data name="Cleanup_ProcessingBranch" xml:space="preserve">
<value>Branch {0} प्रोसेस हो रही है...</value>
</data>
<data name="Cleanup_ArchiveComplete" xml:space="preserve">
<value>संग्रह पूर्ण</value>
</data>
<data name="Cleanup_ArchiveFailed" xml:space="preserve">
<value>Branches संग्रहीत करना विफल: {0}</value>
</data>
<data name="Cleanup_AnalyzingStructure" xml:space="preserve">
<value>Branch संरचना का विश्लेषण हो रहा है...</value>
</data>
<data name="Cleanup_FoundCommits" xml:space="preserve">
<value>रैखिक बनाने के लिए {0} commits मिले...</value>
</data>
<data name="Cleanup_Linearizing" xml:space="preserve">
<value>{0} commits रैखिक हो रहे हैं ({1} merges हटाए जा रहे हैं)...</value>
</data>
<data name="Cleanup_RebuildingCommit" xml:space="preserve">
<value>Commit {0}/{1} का पुनर्निर्माण हो रहा है...</value>
</data>
<data name="Cleanup_Reconciling" xml:space="preserve">
<value>अंतिम स्थिति का समाधान हो रहा है...</value>
</data>
<data name="Cleanup_AlreadyLinear" xml:space="preserve">
<value>इतिहास पहले से ही रैखिक है - कोई merge commits नहीं मिले</value>
</data>
<data name="Cleanup_LinearizeComplete" xml:space="preserve">
<value>रैखिकरण पूर्ण</value>
</data>
<data name="Cleanup_LinearizeFailed" xml:space="preserve">
<value>इतिहास रैखिक बनाना विफल: {0}</value>
</data>
<data name="Cleanup_DescReword" xml:space="preserve">
<value>गुणवत्ता सुधारने के लिए {0} commit संदेश(शों) को फिर से शब्दित किया जाएगा।</value>
</data>
<data name="Cleanup_DescSquash" xml:space="preserve">
<value>{0} डुप्लिकेट commits को 1 में squash किया जाएगा।</value>
</data>
<data name="Cleanup_DescConsolidate" xml:space="preserve">
<value>{0} merge-fix commits समेकित किए जाएंगे।</value>
</data>
<data name="Cleanup_DescAuthorship" xml:space="preserve">
<value>{0} commit(s) पर authorship ठीक की जाएगी।</value>
</data>
<data name="Cleanup_DescTrivialMerges" xml:space="preserve">
<value>{0} तुच्छ merges समेकित किए जाएंगे।</value>
</data>
<data name="Cleanup_DescArchive" xml:space="preserve">
<value>पुरानी branches संग्रहीत की जाएंगी (merge होने पर हटाएं, अन्यथा tag करें)।</value>
</data>
<data name="Cleanup_DescLinearize" xml:space="preserve">
<value>Merge commits हटाकर और तारीख के अनुसार क्रमबद्ध करके इतिहास रैखिक किया जाएगा।</value>
</data>
<data name="Cleanup_DescGeneric" xml:space="preserve">
<value>{0} commit(s) प्रोसेस किए जाएंगे।</value>
</data>
<data name="Cleanup_ReconcileMerge" xml:space="preserve">
<value>समाधान: रैखिकरण के बाद अंतिम स्थिति merge करें</value>
</data>
<!-- ==================== Validation ==================== -->
<data name="Validation_WorkspaceRequired" xml:space="preserve">
<value>WorkspaceRoot आवश्यक है</value>
</data>
<data name="Validation_WorkspaceNotFound" xml:space="preserve">
<value>WorkspaceRoot निर्देशिका मौजूद नहीं है: {0}</value>
</data>
<data name="Validation_MaxCommitsPositive" xml:space="preserve">
<value>MaxCommitsPerRepo 0 से अधिक होना चाहिए</value>
</data>
<data name="Validation_RulesNull" xml:space="preserve">
<value>Rules null नहीं हो सकते</value>
</data>
<data name="Validation_AiOptionsNull" xml:space="preserve">
<value>Ai options null नहीं हो सकते</value>
</data>
<data name="Validation_InvalidOptions" xml:space="preserve">
<value>अमान्य GitImproverOptions: {0}</value>
</data>
<data name="Validation_WeightsSum" xml:space="preserve">
<value>Weights का योग 1.0 होना चाहिए (वर्तमान: {0})</value>
</data>
<!-- ==================== Service Messages ==================== -->
<data name="Service_UnknownError" xml:space="preserve">
<value>अज्ञात त्रुटि</value>
</data>
<data name="Service_RepoNotRegistered" xml:space="preserve">
<value>Repository पंजीकृत नहीं है</value>
</data>
<data name="Service_UncommittedChanges" xml:space="preserve">
<value>असंबद्ध परिवर्तनों के साथ commits को फिर से नहीं लिखा जा सकता। कृपया पहले अपने परिवर्तनों को commit या stash करें।</value>
</data>
<data name="Service_RepoNotFound" xml:space="preserve">
<value>Repository नहीं मिली: {0}</value>
</data>
<data name="Service_NoSuggestion" xml:space="preserve">
<value>कोई सुझाया गया संदेश उपलब्ध नहीं है</value>
</data>
<data name="Service_RepoNotRegisteredPath" xml:space="preserve">
<value>Repository पंजीकृत नहीं है: {0}</value>
</data>
<data name="Service_ApiKeyNotConfigured" xml:space="preserve">
<value>API key कॉन्फ़िगर नहीं है। कृपया सेटिंग्स में अपनी API key सेट करें।</value>
</data>
<data name="Service_AiAnalysisFailed" xml:space="preserve">
<value>AI विश्लेषण विफल</value>
</data>
<data name="Service_AiFallback" xml:space="preserve">
<value>AI ने संरचित आउटपुट वापस नहीं किया - मूल संदेश पर वापस आ गए</value>
</data>
<data name="Service_PushSuccess" xml:space="preserve">
<value>Push सफल</value>
</data>
<!-- ==================== Health Analyzer Status ==================== -->
<data name="Health_LoadingCommits" xml:space="preserve">
<value>Commits लोड हो रहे हैं</value>
</data>
<data name="Health_DetectingDuplicates" xml:space="preserve">
<value>डुप्लिकेट का पता लगाया जा रहा है</value>
</data>
<data name="Health_AnalyzingMerges" xml:space="preserve">
<value>Merge commits का विश्लेषण हो रहा है</value>
</data>
<data name="Health_AnalyzingBranches" xml:space="preserve">
<value>Branch जटिलता का विश्लेषण हो रहा है</value>
</data>
<data name="Health_AnalyzingMessages" xml:space="preserve">
<value>संदेश गुणवत्ता का विश्लेषण हो रहा है</value>
</data>
<data name="Health_AnalyzingAuthorship" xml:space="preserve">
<value>Authorship का विश्लेषण हो रहा है</value>
</data>
<data name="Health_Complete" xml:space="preserve">
<value>पूर्ण</value>
</data>
<!-- ==================== Health Report Issues ==================== -->
<data name="Report_DuplicateContent" xml:space="preserve">
<value>समान सामग्री वाले डुप्लिकेट commits</value>
</data>
<data name="Report_DuplicateContentDesc" xml:space="preserve">
<value>समान फ़ाइल सामग्री वाले commits के {0} समूह मिले ({1} अनावश्यक commits)। इन्हें squash करना सुरक्षित है क्योंकि इनका tree SHA समान है।</value>
</data>
<data name="Report_DuplicateMessages" xml:space="preserve">
<value>डुप्लिकेट संदेशों वाले commits</value>
</data>
<data name="Report_DuplicateMessagesDesc" xml:space="preserve">
<value>समान संदेशों लेकिन विभिन्न कोड परिवर्तनों वाले commits के {0} समूह मिले ({1} commits)। परिवर्तनों को अलग करने के लिए अधिक वर्णनात्मक संदेशों का उपयोग करने पर विचार करें।</value>
</data>
<data name="Report_ExcessiveMerges" xml:space="preserve">
<value>अत्यधिक merge commits</value>
</data>
<data name="Report_HighMergeRatio" xml:space="preserve">
<value>उच्च merge commit अनुपात</value>
</data>
<data name="Report_MergeRatioDesc" xml:space="preserve">
<value>आपकी repository में {0}% merge commit अनुपात है ({1}/{2} commits)। Rebase workflow या squash merges का उपयोग करने पर विचार करें।</value>
</data>
<data name="Report_MergeFixCommits" xml:space="preserve">
<value>Merge fix commits का पता चला</value>
</data>
<data name="Report_MergeFixDesc" xml:space="preserve">
<value>Merges के बाद 'fix merge' जैसे संदेशों वाले {0} commits मिले।</value>
</data>
<data name="Report_CrossMerges" xml:space="preserve">
<value>Branches के बीच cross-merges</value>
</data>
<data name="Report_CrossMergesDesc" xml:space="preserve">
<value>Feature branches के बीच {0} cross-merges का पता चला। केवल main में merge होने वाली feature branches का उपयोग करें।</value>
</data>
<data name="Report_StaleBranches" xml:space="preserve">
<value>पुरानी branches</value>
</data>
<data name="Report_StaleBranchesDesc" xml:space="preserve">
<value>30+ दिनों में कोई गतिविधि नहीं वाली {0} branches मिलीं।</value>
</data>
<!-- ==================== Safety Warnings ==================== -->
<data name="Safety_UncommittedChanges" xml:space="preserve">
<value>आपके पास असंबद्ध परिवर्तन हैं। कृपया पहले उन्हें commit या stash करें।</value>
</data>
<data name="Safety_PushedCommits" xml:space="preserve">
<value>{0} commit(s) पहले ही remote में push किए जा चुके हैं। उन्हें फिर से लिखने के लिए force push की आवश्यकता होगी और यह सहयोगियों को प्रभावित कर सकता है।</value>
</data>
<data name="Safety_BehindRemote" xml:space="preserve">
<value>आपकी branch remote से {0} commit(s) पीछे है। संघर्षों से बचने के लिए पहले pull करने पर विचार करें।</value>
</data>
<!-- ==================== Health Status ==================== -->
<data name="HealthStatus_NeedsAttention" xml:space="preserve">
<value>Repository में ध्यान देने योग्य समस्याएं हैं जिन्हें हल किया जाना चाहिए।</value>
</data>
<data name="HealthStatus_Critical" xml:space="preserve">
<value>Repository को तत्काल ध्यान देने की आवश्यकता है। इतिहास गंभीर रूप से क्षतिग्रस्त है।</value>
</data>
</root>

View File

@@ -0,0 +1,421 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="0" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- ==================== Commit Message Analyzer ==================== -->
<data name="Analyzer_MessageEmpty" xml:space="preserve">
<value>Il messaggio del commit è vuoto</value>
</data>
<data name="Analyzer_SubjectTooShort" xml:space="preserve">
<value>L'oggetto è di {0} caratteri, il minimo è {1}</value>
</data>
<data name="Analyzer_SubjectTooLong" xml:space="preserve">
<value>L'oggetto è di {0} caratteri, il massimo consigliato è {1}</value>
</data>
<data name="Analyzer_BannedPhrase" xml:space="preserve">
<value>L'oggetto usa una frase non descrittiva: '{0}'</value>
</data>
<data name="Analyzer_NotConventional" xml:space="preserve">
<value>Il messaggio non segue il formato conventional commit (tipo: oggetto)</value>
</data>
<data name="Analyzer_UnknownType" xml:space="preserve">
<value>Tipo di conventional commit sconosciuto: {0}</value>
</data>
<data name="Analyzer_NoIssueRef" xml:space="preserve">
<value>Nessun riferimento a issue trovato (es. #123 o JIRA-123)</value>
</data>
<data name="Analyzer_CapitalLetter" xml:space="preserve">
<value>L'oggetto dovrebbe iniziare con una lettera maiuscola</value>
</data>
<data name="Analyzer_NoPeriod" xml:space="preserve">
<value>L'oggetto non dovrebbe terminare con un punto</value>
</data>
<data name="Analyzer_ImperativeMood" xml:space="preserve">
<value>Usa l'imperativo: '{0}' → '{1}' (es. 'Add' non 'Added')</value>
</data>
<data name="Analyzer_BodyTooShort" xml:space="preserve">
<value>Il corpo è di {0} caratteri, il minimo è {1}</value>
</data>
<data name="Analyzer_BlankLine" xml:space="preserve">
<value>Aggiungi una riga vuota tra oggetto e corpo</value>
</data>
<data name="Analyzer_NotDescriptive" xml:space="preserve">
<value>'{0}' non descrive cosa è cambiato in {1} file</value>
</data>
<data name="Analyzer_TooVague" xml:space="preserve">
<value>Il messaggio è troppo vago per {0} file modificati - descrivi COSA è cambiato</value>
</data>
<data name="Analyzer_LargeChange" xml:space="preserve">
<value>Un cambiamento importante ({0} file, {1} righe) merita un messaggio più descrittivo</value>
</data>
<data name="Analyzer_MajorChange" xml:space="preserve">
<value>Un cambiamento importante ({0} file) dovrebbe includere un corpo che spiega il perché</value>
</data>
<data name="Analyzer_MentionArea" xml:space="preserve">
<value>Considera di menzionare quale area è cambiata (file: {0})</value>
</data>
<!-- ==================== Git Operations Service ==================== -->
<data name="Git_NoCommits" xml:space="preserve">
<value>Nessun commit nel repository</value>
</data>
<data name="Git_CommitNotFound" xml:space="preserve">
<value>Commit non trovato: {0}</value>
</data>
<data name="Git_NotAncestor" xml:space="preserve">
<value>Il commit di destinazione non è un antenato di HEAD</value>
</data>
<data name="Git_NoTargetCommits" xml:space="preserve">
<value>Impossibile trovare commit di destinazione nel repository</value>
</data>
<data name="Git_ParentMismatch" xml:space="preserve">
<value>Creazione commit fallita: mancata corrispondenza del genitore per il commit {0}</value>
</data>
<data name="Git_HeadUpdateFailed" xml:space="preserve">
<value>Aggiornamento di HEAD al nuovo commit {0} fallito</value>
</data>
<data name="Git_VerificationFailed" xml:space="preserve">
<value>Verifica disco fallita: HEAD dovrebbe essere {0} ma è {1}</value>
</data>
<data name="Git_OldCommitReachable" xml:space="preserve">
<value>Il vecchio commit {0} è ancora raggiungibile da HEAD dopo la riscrittura</value>
</data>
<data name="Git_Error" xml:space="preserve">
<value>Errore git: {0}</value>
</data>
<data name="Git_RemoteNotFound" xml:space="preserve">
<value>Remote '{0}' non trovato</value>
</data>
<data name="Git_NoUpstreamNoOrigin" xml:space="preserve">
<value>Nessun branch upstream configurato e nessun remote 'origin' trovato.
Imposta il tracking manualmente con: git push -u origin {0}</value>
</data>
<data name="Git_ForcePushSuccess" xml:space="preserve">
<value>Force push eseguito con successo</value>
</data>
<data name="Git_ForcePushedTo" xml:space="preserve">
<value>Force push eseguito su origin/{0}</value>
</data>
<data name="Git_ProcessFailed" xml:space="preserve">
<value>Impossibile avviare il processo git</value>
</data>
<data name="Git_ForcePushSuccessCmd" xml:space="preserve">
<value>Force push eseguito con successo (tramite comando git)</value>
</data>
<data name="Git_PushFailed" xml:space="preserve">
<value>Push fallito: {0}</value>
</data>
<data name="Git_CommandFailed" xml:space="preserve">
<value>Esecuzione comando git fallita: {0}</value>
</data>
<data name="Git_NoUpstream" xml:space="preserve">
<value>Nessun branch upstream configurato. Imposta il tracking con: git push -u origin &lt;branch&gt;</value>
</data>
<data name="Git_NonFastForward" xml:space="preserve">
<value>Push rifiutato: non fast-forward. Esegui pull delle modifiche prima o usa force push.</value>
</data>
<data name="Git_PushSuccessCmd" xml:space="preserve">
<value>Push eseguito con successo (tramite comando git)</value>
</data>
<!-- ==================== Cleanup Executor ==================== -->
<data name="Cleanup_PushedCommitsBlocked" xml:space="preserve">
<value>Alcuni commit sono stati pubblicati. Abilita 'AllowPushedCommits' per procedere.</value>
</data>
<data name="Cleanup_NotImplemented" xml:space="preserve">
<value>Il tipo di pulizia '{0}' non è ancora implementato</value>
</data>
<data name="Cleanup_Rebuilding" xml:space="preserve">
<value>Ricostruzione della cronologia dei commit...</value>
</data>
<data name="Cleanup_RebuildingCount" xml:space="preserve">
<value>Ricostruzione di {0} commit...</value>
</data>
<data name="Cleanup_ProcessingCommit" xml:space="preserve">
<value>Elaborazione commit {0}/{1}...</value>
</data>
<data name="Cleanup_UpdatingBranch" xml:space="preserve">
<value>Aggiornamento del riferimento al branch...</value>
</data>
<data name="Cleanup_SquashingMerges" xml:space="preserve">
<value>Squash dei commit di merge...</value>
</data>
<data name="Cleanup_DropDuplicatesFailed" xml:space="preserve">
<value>Eliminazione dei commit duplicati fallita: {0}</value>
</data>
<data name="Cleanup_NeedTwoCommits" xml:space="preserve">
<value>Servono almeno 2 commit per eseguire lo squash</value>
</data>
<data name="Cleanup_NoCommitsOnBranch" xml:space="preserve">
<value>Nessun commit trovato sul branch corrente</value>
</data>
<data name="Cleanup_NoMatchingCommits" xml:space="preserve">
<value>Nessun commit corrispondente trovato da eliminare</value>
</data>
<data name="Cleanup_NoCommitsToSquash" xml:space="preserve">
<value>Nessun commit specificato per lo squash</value>
</data>
<data name="Cleanup_NoMergeCommits" xml:space="preserve">
<value>Nessun commit di merge corrispondente trovato per lo squash</value>
</data>
<data name="Cleanup_SquashMergeFailed" xml:space="preserve">
<value>Squash dei commit di merge fallito: {0}</value>
</data>
<data name="Cleanup_NoCommitsToFix" xml:space="preserve">
<value>Nessun commit specificato da correggere</value>
</data>
<data name="Cleanup_FixAuthorFailed" xml:space="preserve">
<value>Correzione dell'autore fallita: {0}</value>
</data>
<data name="Cleanup_ConsolidatingFixes" xml:space="preserve">
<value>Consolidamento dei commit di correzione merge...</value>
</data>
<data name="Cleanup_NoFixCommits" xml:space="preserve">
<value>Nessun commit di correzione da consolidare</value>
</data>
<data name="Cleanup_NoMatchingFixes" xml:space="preserve">
<value>Nessun commit di correzione corrispondente trovato da consolidare</value>
</data>
<data name="Cleanup_ConsolidateFailed" xml:space="preserve">
<value>Consolidamento dei commit di correzione merge fallito: {0}</value>
</data>
<data name="Cleanup_ArchivingBranches" xml:space="preserve">
<value>Archiviazione dei branch obsoleti...</value>
</data>
<data name="Cleanup_ProcessingBranch" xml:space="preserve">
<value>Elaborazione branch {0}...</value>
</data>
<data name="Cleanup_ArchiveComplete" xml:space="preserve">
<value>Archiviazione completata</value>
</data>
<data name="Cleanup_ArchiveFailed" xml:space="preserve">
<value>Archiviazione dei branch fallita: {0}</value>
</data>
<data name="Cleanup_AnalyzingStructure" xml:space="preserve">
<value>Analisi della struttura del branch...</value>
</data>
<data name="Cleanup_FoundCommits" xml:space="preserve">
<value>Trovati {0} commit da linearizzare...</value>
</data>
<data name="Cleanup_Linearizing" xml:space="preserve">
<value>Linearizzazione di {0} commit (rimozione di {1} merge)...</value>
</data>
<data name="Cleanup_RebuildingCommit" xml:space="preserve">
<value>Ricostruzione commit {0}/{1}...</value>
</data>
<data name="Cleanup_Reconciling" xml:space="preserve">
<value>Riconciliazione dello stato finale...</value>
</data>
<data name="Cleanup_AlreadyLinear" xml:space="preserve">
<value>La cronologia è già lineare - nessun commit di merge trovato</value>
</data>
<data name="Cleanup_LinearizeComplete" xml:space="preserve">
<value>Linearizzazione completata</value>
</data>
<data name="Cleanup_LinearizeFailed" xml:space="preserve">
<value>Linearizzazione della cronologia fallita: {0}</value>
</data>
<data name="Cleanup_DescReword" xml:space="preserve">
<value>Verrà riformulato il messaggio di {0} commit per migliorarne la qualità.</value>
</data>
<data name="Cleanup_DescSquash" xml:space="preserve">
<value>Verranno uniti {0} commit duplicati in 1.</value>
</data>
<data name="Cleanup_DescConsolidate" xml:space="preserve">
<value>Verranno consolidati {0} commit di correzione merge.</value>
</data>
<data name="Cleanup_DescAuthorship" xml:space="preserve">
<value>Verrà corretta l'autorialità su {0} commit.</value>
</data>
<data name="Cleanup_DescTrivialMerges" xml:space="preserve">
<value>Verranno consolidati {0} merge banali.</value>
</data>
<data name="Cleanup_DescArchive" xml:space="preserve">
<value>Verranno archiviati i branch obsoleti (eliminati se merged, altrimenti taggati).</value>
</data>
<data name="Cleanup_DescLinearize" xml:space="preserve">
<value>Verrà linearizzata la cronologia rimuovendo i commit di merge e ordinando per data.</value>
</data>
<data name="Cleanup_DescGeneric" xml:space="preserve">
<value>Verranno elaborati {0} commit.</value>
</data>
<data name="Cleanup_ReconcileMerge" xml:space="preserve">
<value>Riconciliazione: merge dello stato finale dopo la linearizzazione</value>
</data>
<!-- ==================== Validation ==================== -->
<data name="Validation_WorkspaceRequired" xml:space="preserve">
<value>WorkspaceRoot è obbligatorio</value>
</data>
<data name="Validation_WorkspaceNotFound" xml:space="preserve">
<value>La directory WorkspaceRoot non esiste: {0}</value>
</data>
<data name="Validation_MaxCommitsPositive" xml:space="preserve">
<value>MaxCommitsPerRepo deve essere maggiore di 0</value>
</data>
<data name="Validation_RulesNull" xml:space="preserve">
<value>Rules non può essere null</value>
</data>
<data name="Validation_AiOptionsNull" xml:space="preserve">
<value>Le opzioni Ai non possono essere null</value>
</data>
<data name="Validation_InvalidOptions" xml:space="preserve">
<value>GitImproverOptions non valide: {0}</value>
</data>
<data name="Validation_WeightsSum" xml:space="preserve">
<value>I pesi devono sommare a 1.0 (corrente: {0})</value>
</data>
<!-- ==================== Service Messages ==================== -->
<data name="Service_UnknownError" xml:space="preserve">
<value>Errore sconosciuto</value>
</data>
<data name="Service_RepoNotRegistered" xml:space="preserve">
<value>Repository non registrato</value>
</data>
<data name="Service_UncommittedChanges" xml:space="preserve">
<value>Impossibile riscrivere i commit con modifiche non salvate. Esegui prima il commit o lo stash delle modifiche.</value>
</data>
<data name="Service_RepoNotFound" xml:space="preserve">
<value>Repository non trovato: {0}</value>
</data>
<data name="Service_NoSuggestion" xml:space="preserve">
<value>Nessun messaggio suggerito disponibile</value>
</data>
<data name="Service_RepoNotRegisteredPath" xml:space="preserve">
<value>Repository non registrato: {0}</value>
</data>
<data name="Service_ApiKeyNotConfigured" xml:space="preserve">
<value>La chiave API non è configurata. Imposta la tua chiave API nelle Impostazioni.</value>
</data>
<data name="Service_AiAnalysisFailed" xml:space="preserve">
<value>Analisi AI fallita</value>
</data>
<data name="Service_AiFallback" xml:space="preserve">
<value>L'AI non ha restituito un output strutturato - ripiegato sul messaggio originale</value>
</data>
<data name="Service_PushSuccess" xml:space="preserve">
<value>Push eseguito con successo</value>
</data>
<!-- ==================== Health Analyzer Status ==================== -->
<data name="Health_LoadingCommits" xml:space="preserve">
<value>Caricamento commit</value>
</data>
<data name="Health_DetectingDuplicates" xml:space="preserve">
<value>Rilevamento duplicati</value>
</data>
<data name="Health_AnalyzingMerges" xml:space="preserve">
<value>Analisi dei commit di merge</value>
</data>
<data name="Health_AnalyzingBranches" xml:space="preserve">
<value>Analisi della complessità dei branch</value>
</data>
<data name="Health_AnalyzingMessages" xml:space="preserve">
<value>Analisi della qualità dei messaggi</value>
</data>
<data name="Health_AnalyzingAuthorship" xml:space="preserve">
<value>Analisi dell'autorialità</value>
</data>
<data name="Health_Complete" xml:space="preserve">
<value>Completato</value>
</data>
<!-- ==================== Health Report Issues ==================== -->
<data name="Report_DuplicateContent" xml:space="preserve">
<value>Commit duplicati con contenuto identico</value>
</data>
<data name="Report_DuplicateContentDesc" xml:space="preserve">
<value>Trovati {0} gruppi di commit con contenuto di file identico ({1} commit ridondanti). È sicuro eseguire lo squash poiché hanno lo stesso SHA dell'albero.</value>
</data>
<data name="Report_DuplicateMessages" xml:space="preserve">
<value>Commit con messaggi duplicati</value>
</data>
<data name="Report_DuplicateMessagesDesc" xml:space="preserve">
<value>Trovati {0} gruppi di commit con messaggi identici ma modifiche al codice diverse ({1} commit). Considera l'uso di messaggi più descrittivi per differenziare le modifiche.</value>
</data>
<data name="Report_ExcessiveMerges" xml:space="preserve">
<value>Commit di merge eccessivi</value>
</data>
<data name="Report_HighMergeRatio" xml:space="preserve">
<value>Rapporto elevato di commit di merge</value>
</data>
<data name="Report_MergeRatioDesc" xml:space="preserve">
<value>Il tuo repository ha un rapporto di commit di merge del {0}% ({1}/{2} commit). Considera l'uso del workflow rebase o dello squash merge.</value>
</data>
<data name="Report_MergeFixCommits" xml:space="preserve">
<value>Rilevati commit di correzione merge</value>
</data>
<data name="Report_MergeFixDesc" xml:space="preserve">
<value>Trovati {0} commit con messaggi come 'fix merge' rilevati dopo i merge.</value>
</data>
<data name="Report_CrossMerges" xml:space="preserve">
<value>Cross-merge tra branch</value>
</data>
<data name="Report_CrossMergesDesc" xml:space="preserve">
<value>Rilevati {0} cross-merge tra branch di funzionalità. Usa branch di funzionalità che si uniscono solo al main.</value>
</data>
<data name="Report_StaleBranches" xml:space="preserve">
<value>Branch obsoleti</value>
</data>
<data name="Report_StaleBranchesDesc" xml:space="preserve">
<value>Trovati {0} branch senza attività negli ultimi 30+ giorni.</value>
</data>
<!-- ==================== Safety Warnings ==================== -->
<data name="Safety_UncommittedChanges" xml:space="preserve">
<value>Hai modifiche non salvate. Esegui prima il commit o lo stash.</value>
</data>
<data name="Safety_PushedCommits" xml:space="preserve">
<value>{0} commit sono già stati pubblicati sul remote. Riscriverli richiederà un force push e potrebbe influire sui collaboratori.</value>
</data>
<data name="Safety_BehindRemote" xml:space="preserve">
<value>Il tuo branch è {0} commit indietro rispetto al remote. Considera di eseguire pull prima per evitare conflitti.</value>
</data>
<!-- ==================== Health Status ==================== -->
<data name="HealthStatus_NeedsAttention" xml:space="preserve">
<value>Il repository presenta problemi evidenti che dovrebbero essere risolti.</value>
</data>
<data name="HealthStatus_Critical" xml:space="preserve">
<value>Il repository richiede attenzione immediata. La cronologia è gravemente degradata.</value>
</data>
</root>

View File

@@ -0,0 +1,421 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="0" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- ==================== Commit Message Analyzer ==================== -->
<data name="Analyzer_MessageEmpty" xml:space="preserve">
<value>commitメッセージが空です</value>
</data>
<data name="Analyzer_SubjectTooShort" xml:space="preserve">
<value>件名が{0}文字です。最小値は{1}文字です</value>
</data>
<data name="Analyzer_SubjectTooLong" xml:space="preserve">
<value>件名が{0}文字です。推奨最大値は{1}文字です</value>
</data>
<data name="Analyzer_BannedPhrase" xml:space="preserve">
<value>件名に説明不足のフレーズが使用されています: '{0}'</value>
</data>
<data name="Analyzer_NotConventional" xml:space="preserve">
<value>メッセージがconventional commit形式に従っていません (type: subject)</value>
</data>
<data name="Analyzer_UnknownType" xml:space="preserve">
<value>不明なconventional commitタイプ: {0}</value>
</data>
<data name="Analyzer_NoIssueRef" xml:space="preserve">
<value>課題参照が見つかりません (例: #123 または JIRA-123)</value>
</data>
<data name="Analyzer_CapitalLetter" xml:space="preserve">
<value>件名は大文字で始める必要があります</value>
</data>
<data name="Analyzer_NoPeriod" xml:space="preserve">
<value>件名の末尾にピリオドを付けないでください</value>
</data>
<data name="Analyzer_ImperativeMood" xml:space="preserve">
<value>命令形を使用してください: '{0}' → '{1}' (例: 'Add' であって 'Added' ではありません)</value>
</data>
<data name="Analyzer_BodyTooShort" xml:space="preserve">
<value>本文が{0}文字です。最小値は{1}文字です</value>
</data>
<data name="Analyzer_BlankLine" xml:space="preserve">
<value>件名と本文の間に空白行を追加してください</value>
</data>
<data name="Analyzer_NotDescriptive" xml:space="preserve">
<value>'{0}'は{1}個のファイルで何が変更されたかを説明していません</value>
</data>
<data name="Analyzer_TooVague" xml:space="preserve">
<value>変更された{0}個のファイルに対してメッセージが曖昧すぎます - 何が変更されたかを説明してください</value>
</data>
<data name="Analyzer_LargeChange" xml:space="preserve">
<value>大規模な変更 ({0}個のファイル、{1}行) にはより詳細なメッセージが必要です</value>
</data>
<data name="Analyzer_MajorChange" xml:space="preserve">
<value>主要な変更 ({0}個のファイル) には理由を説明する本文を含める必要があります</value>
</data>
<data name="Analyzer_MentionArea" xml:space="preserve">
<value>変更された領域の記載を検討してください (ファイル: {0})</value>
</data>
<!-- ==================== Git Operations Service ==================== -->
<data name="Git_NoCommits" xml:space="preserve">
<value>リポジトリにcommitがありません</value>
</data>
<data name="Git_CommitNotFound" xml:space="preserve">
<value>commitが見つかりません: {0}</value>
</data>
<data name="Git_NotAncestor" xml:space="preserve">
<value>対象のcommitはHEADの祖先ではありません</value>
</data>
<data name="Git_NoTargetCommits" xml:space="preserve">
<value>リポジトリで対象のcommitが見つかりませんでした</value>
</data>
<data name="Git_ParentMismatch" xml:space="preserve">
<value>commit作成に失敗しました: commit {0}の親が一致しません</value>
</data>
<data name="Git_HeadUpdateFailed" xml:space="preserve">
<value>HEADを新しいcommit {0}に更新できませんでした</value>
</data>
<data name="Git_VerificationFailed" xml:space="preserve">
<value>ディスク検証に失敗しました: HEADは{0}であるべきですが、{1}です</value>
</data>
<data name="Git_OldCommitReachable" xml:space="preserve">
<value>書き換え後、古いcommit {0}がHEADから到達可能です</value>
</data>
<data name="Git_Error" xml:space="preserve">
<value>Gitエラー: {0}</value>
</data>
<data name="Git_RemoteNotFound" xml:space="preserve">
<value>リモート'{0}'が見つかりません</value>
</data>
<data name="Git_NoUpstreamNoOrigin" xml:space="preserve">
<value>upstreamブランチが設定されておらず、'origin'リモートも見つかりません。
次のコマンドで手動で追跡を設定してください: git push -u origin {0}</value>
</data>
<data name="Git_ForcePushSuccess" xml:space="preserve">
<value>force pushが成功しました</value>
</data>
<data name="Git_ForcePushedTo" xml:space="preserve">
<value>origin/{0}にforce pushしました</value>
</data>
<data name="Git_ProcessFailed" xml:space="preserve">
<value>gitプロセスの起動に失敗しました</value>
</data>
<data name="Git_ForcePushSuccessCmd" xml:space="preserve">
<value>force pushが成功しました (gitコマンド経由)</value>
</data>
<data name="Git_PushFailed" xml:space="preserve">
<value>pushに失敗しました: {0}</value>
</data>
<data name="Git_CommandFailed" xml:space="preserve">
<value>gitコマンドの実行に失敗しました: {0}</value>
</data>
<data name="Git_NoUpstream" xml:space="preserve">
<value>upstreamブランチが設定されていません。次のコマンドで追跡を設定してください: git push -u origin &lt;branch&gt;</value>
</data>
<data name="Git_NonFastForward" xml:space="preserve">
<value>pushが拒否されました: non-fast-forward。先に変更をpullするか、force pushを使用してください。</value>
</data>
<data name="Git_PushSuccessCmd" xml:space="preserve">
<value>pushが成功しました (gitコマンド経由)</value>
</data>
<!-- ==================== Cleanup Executor ==================== -->
<data name="Cleanup_PushedCommitsBlocked" xml:space="preserve">
<value>一部のcommitがpushされています。続行するには'AllowPushedCommits'を有効にしてください。</value>
</data>
<data name="Cleanup_NotImplemented" xml:space="preserve">
<value>クリーンアップタイプ'{0}'はまだ実装されていません</value>
</data>
<data name="Cleanup_Rebuilding" xml:space="preserve">
<value>commit履歴を再構築しています...</value>
</data>
<data name="Cleanup_RebuildingCount" xml:space="preserve">
<value>{0}個のcommitを再構築しています...</value>
</data>
<data name="Cleanup_ProcessingCommit" xml:space="preserve">
<value>commit {0}/{1}を処理しています...</value>
</data>
<data name="Cleanup_UpdatingBranch" xml:space="preserve">
<value>branch参照を更新しています...</value>
</data>
<data name="Cleanup_SquashingMerges" xml:space="preserve">
<value>merge commitをsquashしています...</value>
</data>
<data name="Cleanup_DropDuplicatesFailed" xml:space="preserve">
<value>重複commitの削除に失敗しました: {0}</value>
</data>
<data name="Cleanup_NeedTwoCommits" xml:space="preserve">
<value>squashするには少なくとも2つのcommitが必要です</value>
</data>
<data name="Cleanup_NoCommitsOnBranch" xml:space="preserve">
<value>現在のbranchにcommitが見つかりません</value>
</data>
<data name="Cleanup_NoMatchingCommits" xml:space="preserve">
<value>削除する一致するcommitが見つかりません</value>
</data>
<data name="Cleanup_NoCommitsToSquash" xml:space="preserve">
<value>squashするcommitが指定されていません</value>
</data>
<data name="Cleanup_NoMergeCommits" xml:space="preserve">
<value>squashする一致するmerge commitが見つかりません</value>
</data>
<data name="Cleanup_SquashMergeFailed" xml:space="preserve">
<value>merge commitのsquashに失敗しました: {0}</value>
</data>
<data name="Cleanup_NoCommitsToFix" xml:space="preserve">
<value>修正するcommitが指定されていません</value>
</data>
<data name="Cleanup_FixAuthorFailed" xml:space="preserve">
<value>作成者の修正に失敗しました: {0}</value>
</data>
<data name="Cleanup_ConsolidatingFixes" xml:space="preserve">
<value>merge修正commitを統合しています...</value>
</data>
<data name="Cleanup_NoFixCommits" xml:space="preserve">
<value>統合する修正commitがありません</value>
</data>
<data name="Cleanup_NoMatchingFixes" xml:space="preserve">
<value>統合する一致する修正commitが見つかりません</value>
</data>
<data name="Cleanup_ConsolidateFailed" xml:space="preserve">
<value>merge修正commitの統合に失敗しました: {0}</value>
</data>
<data name="Cleanup_ArchivingBranches" xml:space="preserve">
<value>古いbranchをアーカイブしています...</value>
</data>
<data name="Cleanup_ProcessingBranch" xml:space="preserve">
<value>branch {0}を処理しています...</value>
</data>
<data name="Cleanup_ArchiveComplete" xml:space="preserve">
<value>アーカイブが完了しました</value>
</data>
<data name="Cleanup_ArchiveFailed" xml:space="preserve">
<value>branchのアーカイブに失敗しました: {0}</value>
</data>
<data name="Cleanup_AnalyzingStructure" xml:space="preserve">
<value>branch構造を分析しています...</value>
</data>
<data name="Cleanup_FoundCommits" xml:space="preserve">
<value>線形化する{0}個のcommitが見つかりました...</value>
</data>
<data name="Cleanup_Linearizing" xml:space="preserve">
<value>{0}個のcommitを線形化しています ({1}個のmergeを削除)...</value>
</data>
<data name="Cleanup_RebuildingCommit" xml:space="preserve">
<value>commit {0}/{1}を再構築しています...</value>
</data>
<data name="Cleanup_Reconciling" xml:space="preserve">
<value>最終状態を調整しています...</value>
</data>
<data name="Cleanup_AlreadyLinear" xml:space="preserve">
<value>履歴はすでに線形です - merge commitが見つかりません</value>
</data>
<data name="Cleanup_LinearizeComplete" xml:space="preserve">
<value>線形化が完了しました</value>
</data>
<data name="Cleanup_LinearizeFailed" xml:space="preserve">
<value>履歴の線形化に失敗しました: {0}</value>
</data>
<data name="Cleanup_DescReword" xml:space="preserve">
<value>品質向上のため、{0}個のcommitメッセージを書き直します。</value>
</data>
<data name="Cleanup_DescSquash" xml:space="preserve">
<value>{0}個の重複commitを1つにsquashします。</value>
</data>
<data name="Cleanup_DescConsolidate" xml:space="preserve">
<value>{0}個のmerge修正commitを統合します。</value>
</data>
<data name="Cleanup_DescAuthorship" xml:space="preserve">
<value>{0}個のcommitの作成者を修正します。</value>
</data>
<data name="Cleanup_DescTrivialMerges" xml:space="preserve">
<value>{0}個の些細なmergeを統合します。</value>
</data>
<data name="Cleanup_DescArchive" xml:space="preserve">
<value>古いbranchをアーカイブします (mergeされている場合は削除、そうでない場合はタグ付け)。</value>
</data>
<data name="Cleanup_DescLinearize" xml:space="preserve">
<value>merge commitを削除し、日付順にソートして履歴を線形化します。</value>
</data>
<data name="Cleanup_DescGeneric" xml:space="preserve">
<value>{0}個のcommitを処理します。</value>
</data>
<data name="Cleanup_ReconcileMerge" xml:space="preserve">
<value>調整: 線形化後の最終状態をmerge</value>
</data>
<!-- ==================== Validation ==================== -->
<data name="Validation_WorkspaceRequired" xml:space="preserve">
<value>WorkspaceRootは必須です</value>
</data>
<data name="Validation_WorkspaceNotFound" xml:space="preserve">
<value>WorkspaceRootディレクトリが存在しません: {0}</value>
</data>
<data name="Validation_MaxCommitsPositive" xml:space="preserve">
<value>MaxCommitsPerRepoは0より大きい値である必要があります</value>
</data>
<data name="Validation_RulesNull" xml:space="preserve">
<value>Rulesをnullにすることはできません</value>
</data>
<data name="Validation_AiOptionsNull" xml:space="preserve">
<value>Aiオプションをnullにすることはできません</value>
</data>
<data name="Validation_InvalidOptions" xml:space="preserve">
<value>無効なGitImproverOptions: {0}</value>
</data>
<data name="Validation_WeightsSum" xml:space="preserve">
<value>重みの合計は1.0である必要があります (現在: {0})</value>
</data>
<!-- ==================== Service Messages ==================== -->
<data name="Service_UnknownError" xml:space="preserve">
<value>不明なエラー</value>
</data>
<data name="Service_RepoNotRegistered" xml:space="preserve">
<value>リポジトリが登録されていません</value>
</data>
<data name="Service_UncommittedChanges" xml:space="preserve">
<value>コミットされていない変更があるため、commitを書き換えることができません。先に変更をcommitまたはstashしてください。</value>
</data>
<data name="Service_RepoNotFound" xml:space="preserve">
<value>リポジトリが見つかりません: {0}</value>
</data>
<data name="Service_NoSuggestion" xml:space="preserve">
<value>提案されたメッセージがありません</value>
</data>
<data name="Service_RepoNotRegisteredPath" xml:space="preserve">
<value>リポジトリが登録されていません: {0}</value>
</data>
<data name="Service_ApiKeyNotConfigured" xml:space="preserve">
<value>APIキーが設定されていません。設定でAPIキーを設定してください。</value>
</data>
<data name="Service_AiAnalysisFailed" xml:space="preserve">
<value>AI分析に失敗しました</value>
</data>
<data name="Service_AiFallback" xml:space="preserve">
<value>AIが構造化された出力を返しませんでした - 元のメッセージにフォールバックしました</value>
</data>
<data name="Service_PushSuccess" xml:space="preserve">
<value>pushが成功しました</value>
</data>
<!-- ==================== Health Analyzer Status ==================== -->
<data name="Health_LoadingCommits" xml:space="preserve">
<value>commitを読み込んでいます</value>
</data>
<data name="Health_DetectingDuplicates" xml:space="preserve">
<value>重複を検出しています</value>
</data>
<data name="Health_AnalyzingMerges" xml:space="preserve">
<value>merge commitを分析しています</value>
</data>
<data name="Health_AnalyzingBranches" xml:space="preserve">
<value>branchの複雑さを分析しています</value>
</data>
<data name="Health_AnalyzingMessages" xml:space="preserve">
<value>メッセージの品質を分析しています</value>
</data>
<data name="Health_AnalyzingAuthorship" xml:space="preserve">
<value>作成者を分析しています</value>
</data>
<data name="Health_Complete" xml:space="preserve">
<value>完了</value>
</data>
<!-- ==================== Health Report Issues ==================== -->
<data name="Report_DuplicateContent" xml:space="preserve">
<value>内容が同一の重複commit</value>
</data>
<data name="Report_DuplicateContentDesc" xml:space="preserve">
<value>同一のファイル内容を持つcommitのグループが{0}個見つかりました ({1}個の冗長なcommit)。これらは同じツリーSHAを持つため、安全にsquashできます。</value>
</data>
<data name="Report_DuplicateMessages" xml:space="preserve">
<value>重複したメッセージを持つcommit</value>
</data>
<data name="Report_DuplicateMessagesDesc" xml:space="preserve">
<value>同一のメッセージを持つが、異なるコード変更があるcommitのグループが{0}個見つかりました ({1}個のcommit)。変更を区別するために、より説明的なメッセージの使用を検討してください。</value>
</data>
<data name="Report_ExcessiveMerges" xml:space="preserve">
<value>過剰なmerge commit</value>
</data>
<data name="Report_HighMergeRatio" xml:space="preserve">
<value>merge commitの比率が高い</value>
</data>
<data name="Report_MergeRatioDesc" xml:space="preserve">
<value>リポジトリのmerge commit比率が{0}%です ({1}/{2}個のcommit)。rebaseワークフローまたはsquash mergeの使用を検討してください。</value>
</data>
<data name="Report_MergeFixCommits" xml:space="preserve">
<value>merge修正commitが検出されました</value>
</data>
<data name="Report_MergeFixDesc" xml:space="preserve">
<value>merge後に'fix merge'のようなメッセージを持つcommitが{0}個見つかりました。</value>
</data>
<data name="Report_CrossMerges" xml:space="preserve">
<value>branch間のクロスmerge</value>
</data>
<data name="Report_CrossMergesDesc" xml:space="preserve">
<value>機能branch間のクロスmergeが{0}個検出されました。mainにのみmergeする機能branchを使用してください。</value>
</data>
<data name="Report_StaleBranches" xml:space="preserve">
<value>古いbranch</value>
</data>
<data name="Report_StaleBranchesDesc" xml:space="preserve">
<value>30日以上活動がないbranchが{0}個見つかりました。</value>
</data>
<!-- ==================== Safety Warnings ==================== -->
<data name="Safety_UncommittedChanges" xml:space="preserve">
<value>コミットされていない変更があります。先にcommitまたはstashしてください。</value>
</data>
<data name="Safety_PushedCommits" xml:space="preserve">
<value>{0}個のcommitがすでにリモートにpushされています。これらを書き換えるにはforce pushが必要で、共同作業者に影響を与える可能性があります。</value>
</data>
<data name="Safety_BehindRemote" xml:space="preserve">
<value>branchがリモートより{0}個のcommit分遅れています。競合を避けるため、先にpullすることを検討してください。</value>
</data>
<!-- ==================== Health Status ==================== -->
<data name="HealthStatus_NeedsAttention" xml:space="preserve">
<value>リポジトリに対処すべき目立った問題があります。</value>
</data>
<data name="HealthStatus_Critical" xml:space="preserve">
<value>リポジトリは緊急の対応が必要です。履歴が著しく劣化しています。</value>
</data>
</root>

View File

@@ -0,0 +1,421 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="0" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- ==================== Commit Message Analyzer ==================== -->
<data name="Analyzer_MessageEmpty" xml:space="preserve">
<value>Commit-bericht is leeg</value>
</data>
<data name="Analyzer_SubjectTooShort" xml:space="preserve">
<value>Onderwerp is {0} tekens, minimum is {1}</value>
</data>
<data name="Analyzer_SubjectTooLong" xml:space="preserve">
<value>Onderwerp is {0} tekens, aanbevolen maximum is {1}</value>
</data>
<data name="Analyzer_BannedPhrase" xml:space="preserve">
<value>Onderwerp gebruikt niet-beschrijvende zin: '{0}'</value>
</data>
<data name="Analyzer_NotConventional" xml:space="preserve">
<value>Bericht volgt niet het conventionele commit-formaat (type: onderwerp)</value>
</data>
<data name="Analyzer_UnknownType" xml:space="preserve">
<value>Onbekend conventioneel commit-type: {0}</value>
</data>
<data name="Analyzer_NoIssueRef" xml:space="preserve">
<value>Geen issue-referentie gevonden (bijv. #123 of JIRA-123)</value>
</data>
<data name="Analyzer_CapitalLetter" xml:space="preserve">
<value>Onderwerp moet beginnen met een hoofdletter</value>
</data>
<data name="Analyzer_NoPeriod" xml:space="preserve">
<value>Onderwerp mag niet eindigen met een punt</value>
</data>
<data name="Analyzer_ImperativeMood" xml:space="preserve">
<value>Gebruik gebiedende wijs: '{0}' → '{1}' (bijv. 'Voeg toe' niet 'Toegevoegd')</value>
</data>
<data name="Analyzer_BodyTooShort" xml:space="preserve">
<value>Body is {0} tekens, minimum is {1}</value>
</data>
<data name="Analyzer_BlankLine" xml:space="preserve">
<value>Voeg een lege regel toe tussen onderwerp en body</value>
</data>
<data name="Analyzer_NotDescriptive" xml:space="preserve">
<value>'{0}' beschrijft niet wat er is gewijzigd in {1} bestanden</value>
</data>
<data name="Analyzer_TooVague" xml:space="preserve">
<value>Bericht is te vaag voor {0} gewijzigde bestanden - beschrijf WAT er is gewijzigd</value>
</data>
<data name="Analyzer_LargeChange" xml:space="preserve">
<value>Grote wijziging ({0} bestanden, {1} regels) verdient een meer beschrijvend bericht</value>
</data>
<data name="Analyzer_MajorChange" xml:space="preserve">
<value>Grote wijziging ({0} bestanden) moet een body bevatten die uitlegt waarom</value>
</data>
<data name="Analyzer_MentionArea" xml:space="preserve">
<value>Overweeg te vermelden welk gebied is gewijzigd (bestanden: {0})</value>
</data>
<!-- ==================== Git Operations Service ==================== -->
<data name="Git_NoCommits" xml:space="preserve">
<value>Geen commits in repository</value>
</data>
<data name="Git_CommitNotFound" xml:space="preserve">
<value>Commit niet gevonden: {0}</value>
</data>
<data name="Git_NotAncestor" xml:space="preserve">
<value>Doel-commit is geen voorouder van HEAD</value>
</data>
<data name="Git_NoTargetCommits" xml:space="preserve">
<value>Kon geen doel-commits vinden in repository</value>
</data>
<data name="Git_ParentMismatch" xml:space="preserve">
<value>Aanmaken van commit mislukt: parent komt niet overeen voor commit {0}</value>
</data>
<data name="Git_HeadUpdateFailed" xml:space="preserve">
<value>Bijwerken van HEAD naar nieuwe commit {0} mislukt</value>
</data>
<data name="Git_VerificationFailed" xml:space="preserve">
<value>Schijfverificatie mislukt: HEAD zou {0} moeten zijn maar is {1}</value>
</data>
<data name="Git_OldCommitReachable" xml:space="preserve">
<value>Oude commit {0} nog steeds bereikbaar vanaf HEAD na herschrijven</value>
</data>
<data name="Git_Error" xml:space="preserve">
<value>Git-fout: {0}</value>
</data>
<data name="Git_RemoteNotFound" xml:space="preserve">
<value>Remote '{0}' niet gevonden</value>
</data>
<data name="Git_NoUpstreamNoOrigin" xml:space="preserve">
<value>Geen upstream branch geconfigureerd en geen 'origin' remote gevonden.
Stel tracking handmatig in met: git push -u origin {0}</value>
</data>
<data name="Git_ForcePushSuccess" xml:space="preserve">
<value>Force push succesvol</value>
</data>
<data name="Git_ForcePushedTo" xml:space="preserve">
<value>Force push naar origin/{0} uitgevoerd</value>
</data>
<data name="Git_ProcessFailed" xml:space="preserve">
<value>Starten van git-proces mislukt</value>
</data>
<data name="Git_ForcePushSuccessCmd" xml:space="preserve">
<value>Force push succesvol (via git-commando)</value>
</data>
<data name="Git_PushFailed" xml:space="preserve">
<value>Push mislukt: {0}</value>
</data>
<data name="Git_CommandFailed" xml:space="preserve">
<value>Uitvoeren van git-commando mislukt: {0}</value>
</data>
<data name="Git_NoUpstream" xml:space="preserve">
<value>Geen upstream branch geconfigureerd. Stel tracking in met: git push -u origin &lt;branch&gt;</value>
</data>
<data name="Git_NonFastForward" xml:space="preserve">
<value>Push geweigerd: non-fast-forward. Haal eerst wijzigingen op of gebruik force push.</value>
</data>
<data name="Git_PushSuccessCmd" xml:space="preserve">
<value>Push succesvol (via git-commando)</value>
</data>
<!-- ==================== Cleanup Executor ==================== -->
<data name="Cleanup_PushedCommitsBlocked" xml:space="preserve">
<value>Sommige commits zijn gepusht. Schakel 'AllowPushedCommits' in om door te gaan.</value>
</data>
<data name="Cleanup_NotImplemented" xml:space="preserve">
<value>Opruimtype '{0}' is nog niet geïmplementeerd</value>
</data>
<data name="Cleanup_Rebuilding" xml:space="preserve">
<value>Commit-geschiedenis wordt opnieuw opgebouwd...</value>
</data>
<data name="Cleanup_RebuildingCount" xml:space="preserve">
<value>{0} commits worden opnieuw opgebouwd...</value>
</data>
<data name="Cleanup_ProcessingCommit" xml:space="preserve">
<value>Commit {0}/{1} wordt verwerkt...</value>
</data>
<data name="Cleanup_UpdatingBranch" xml:space="preserve">
<value>Branch-referentie wordt bijgewerkt...</value>
</data>
<data name="Cleanup_SquashingMerges" xml:space="preserve">
<value>Merge-commits worden samengevoegd...</value>
</data>
<data name="Cleanup_DropDuplicatesFailed" xml:space="preserve">
<value>Verwijderen van dubbele commits mislukt: {0}</value>
</data>
<data name="Cleanup_NeedTwoCommits" xml:space="preserve">
<value>Minimaal 2 commits nodig om samen te voegen</value>
</data>
<data name="Cleanup_NoCommitsOnBranch" xml:space="preserve">
<value>Geen commits gevonden op huidige branch</value>
</data>
<data name="Cleanup_NoMatchingCommits" xml:space="preserve">
<value>Geen overeenkomende commits gevonden om te verwijderen</value>
</data>
<data name="Cleanup_NoCommitsToSquash" xml:space="preserve">
<value>Geen commits opgegeven om samen te voegen</value>
</data>
<data name="Cleanup_NoMergeCommits" xml:space="preserve">
<value>Geen overeenkomende merge-commits gevonden om samen te voegen</value>
</data>
<data name="Cleanup_SquashMergeFailed" xml:space="preserve">
<value>Samenvoegen van merge-commits mislukt: {0}</value>
</data>
<data name="Cleanup_NoCommitsToFix" xml:space="preserve">
<value>Geen commits opgegeven om te corrigeren</value>
</data>
<data name="Cleanup_FixAuthorFailed" xml:space="preserve">
<value>Corrigeren van auteurschap mislukt: {0}</value>
</data>
<data name="Cleanup_ConsolidatingFixes" xml:space="preserve">
<value>Merge-fix commits worden geconsolideerd...</value>
</data>
<data name="Cleanup_NoFixCommits" xml:space="preserve">
<value>Geen fix-commits om te consolideren</value>
</data>
<data name="Cleanup_NoMatchingFixes" xml:space="preserve">
<value>Geen overeenkomende fix-commits gevonden om te consolideren</value>
</data>
<data name="Cleanup_ConsolidateFailed" xml:space="preserve">
<value>Consolideren van merge-fix commits mislukt: {0}</value>
</data>
<data name="Cleanup_ArchivingBranches" xml:space="preserve">
<value>Verouderde branches worden gearchiveerd...</value>
</data>
<data name="Cleanup_ProcessingBranch" xml:space="preserve">
<value>Branch {0} wordt verwerkt...</value>
</data>
<data name="Cleanup_ArchiveComplete" xml:space="preserve">
<value>Archivering voltooid</value>
</data>
<data name="Cleanup_ArchiveFailed" xml:space="preserve">
<value>Archiveren van branches mislukt: {0}</value>
</data>
<data name="Cleanup_AnalyzingStructure" xml:space="preserve">
<value>Branch-structuur wordt geanalyseerd...</value>
</data>
<data name="Cleanup_FoundCommits" xml:space="preserve">
<value>{0} commits gevonden om te lineariseren...</value>
</data>
<data name="Cleanup_Linearizing" xml:space="preserve">
<value>{0} commits worden gelineariseerd ({1} merges worden verwijderd)...</value>
</data>
<data name="Cleanup_RebuildingCommit" xml:space="preserve">
<value>Commit {0}/{1} wordt opnieuw opgebouwd...</value>
</data>
<data name="Cleanup_Reconciling" xml:space="preserve">
<value>Eindstaat wordt gereconcilieerd...</value>
</data>
<data name="Cleanup_AlreadyLinear" xml:space="preserve">
<value>Geschiedenis is al lineair - geen merge-commits gevonden</value>
</data>
<data name="Cleanup_LinearizeComplete" xml:space="preserve">
<value>Linearisatie voltooid</value>
</data>
<data name="Cleanup_LinearizeFailed" xml:space="preserve">
<value>Lineariseren van geschiedenis mislukt: {0}</value>
</data>
<data name="Cleanup_DescReword" xml:space="preserve">
<value>{0} commit-bericht(en) zullen worden herschreven om de kwaliteit te verbeteren.</value>
</data>
<data name="Cleanup_DescSquash" xml:space="preserve">
<value>{0} dubbele commits zullen worden samengevoegd tot 1.</value>
</data>
<data name="Cleanup_DescConsolidate" xml:space="preserve">
<value>{0} merge-fix commits zullen worden geconsolideerd.</value>
</data>
<data name="Cleanup_DescAuthorship" xml:space="preserve">
<value>Auteurschap zal worden gecorrigeerd op {0} commit(s).</value>
</data>
<data name="Cleanup_DescTrivialMerges" xml:space="preserve">
<value>{0} triviale merges zullen worden geconsolideerd.</value>
</data>
<data name="Cleanup_DescArchive" xml:space="preserve">
<value>Verouderde branches zullen worden gearchiveerd (verwijderen indien gemerged, anders taggen).</value>
</data>
<data name="Cleanup_DescLinearize" xml:space="preserve">
<value>Geschiedenis zal worden gelineariseerd door merge-commits te verwijderen en op datum te sorteren.</value>
</data>
<data name="Cleanup_DescGeneric" xml:space="preserve">
<value>{0} commit(s) zullen worden verwerkt.</value>
</data>
<data name="Cleanup_ReconcileMerge" xml:space="preserve">
<value>Reconcile: eindstaat samenvoegen na linearisatie</value>
</data>
<!-- ==================== Validation ==================== -->
<data name="Validation_WorkspaceRequired" xml:space="preserve">
<value>WorkspaceRoot is verplicht</value>
</data>
<data name="Validation_WorkspaceNotFound" xml:space="preserve">
<value>WorkspaceRoot-map bestaat niet: {0}</value>
</data>
<data name="Validation_MaxCommitsPositive" xml:space="preserve">
<value>MaxCommitsPerRepo moet groter zijn dan 0</value>
</data>
<data name="Validation_RulesNull" xml:space="preserve">
<value>Rules kunnen niet null zijn</value>
</data>
<data name="Validation_AiOptionsNull" xml:space="preserve">
<value>Ai-opties kunnen niet null zijn</value>
</data>
<data name="Validation_InvalidOptions" xml:space="preserve">
<value>Ongeldige GitImproverOptions: {0}</value>
</data>
<data name="Validation_WeightsSum" xml:space="preserve">
<value>Gewichten moeten optellen tot 1.0 (huidig: {0})</value>
</data>
<!-- ==================== Service Messages ==================== -->
<data name="Service_UnknownError" xml:space="preserve">
<value>Onbekende fout</value>
</data>
<data name="Service_RepoNotRegistered" xml:space="preserve">
<value>Repository niet geregistreerd</value>
</data>
<data name="Service_UncommittedChanges" xml:space="preserve">
<value>Kan commits niet herschrijven met niet-gecommitte wijzigingen. Commit of stash eerst uw wijzigingen.</value>
</data>
<data name="Service_RepoNotFound" xml:space="preserve">
<value>Repository niet gevonden: {0}</value>
</data>
<data name="Service_NoSuggestion" xml:space="preserve">
<value>Geen gesuggereerd bericht beschikbaar</value>
</data>
<data name="Service_RepoNotRegisteredPath" xml:space="preserve">
<value>Repository niet geregistreerd: {0}</value>
</data>
<data name="Service_ApiKeyNotConfigured" xml:space="preserve">
<value>API-sleutel is niet geconfigureerd. Stel uw API-sleutel in via Instellingen.</value>
</data>
<data name="Service_AiAnalysisFailed" xml:space="preserve">
<value>AI-analyse mislukt</value>
</data>
<data name="Service_AiFallback" xml:space="preserve">
<value>AI heeft geen gestructureerde output gegeven - teruggevallen op origineel bericht</value>
</data>
<data name="Service_PushSuccess" xml:space="preserve">
<value>Push succesvol</value>
</data>
<!-- ==================== Health Analyzer Status ==================== -->
<data name="Health_LoadingCommits" xml:space="preserve">
<value>Commits worden geladen</value>
</data>
<data name="Health_DetectingDuplicates" xml:space="preserve">
<value>Duplicaten worden gedetecteerd</value>
</data>
<data name="Health_AnalyzingMerges" xml:space="preserve">
<value>Merge-commits worden geanalyseerd</value>
</data>
<data name="Health_AnalyzingBranches" xml:space="preserve">
<value>Branch-complexiteit wordt geanalyseerd</value>
</data>
<data name="Health_AnalyzingMessages" xml:space="preserve">
<value>Berichtkwaliteit wordt geanalyseerd</value>
</data>
<data name="Health_AnalyzingAuthorship" xml:space="preserve">
<value>Auteurschap wordt geanalyseerd</value>
</data>
<data name="Health_Complete" xml:space="preserve">
<value>Voltooid</value>
</data>
<!-- ==================== Health Report Issues ==================== -->
<data name="Report_DuplicateContent" xml:space="preserve">
<value>Dubbele commits met identieke inhoud</value>
</data>
<data name="Report_DuplicateContentDesc" xml:space="preserve">
<value>{0} groepen van commits gevonden met identieke bestandsinhoud ({1} redundante commits). Deze kunnen veilig worden samengevoegd omdat ze dezelfde tree SHA hebben.</value>
</data>
<data name="Report_DuplicateMessages" xml:space="preserve">
<value>Commits met dubbele berichten</value>
</data>
<data name="Report_DuplicateMessagesDesc" xml:space="preserve">
<value>{0} groepen van commits gevonden met identieke berichten maar verschillende codewijzigingen ({1} commits). Overweeg meer beschrijvende berichten te gebruiken om wijzigingen te onderscheiden.</value>
</data>
<data name="Report_ExcessiveMerges" xml:space="preserve">
<value>Buitensporig veel merge-commits</value>
</data>
<data name="Report_HighMergeRatio" xml:space="preserve">
<value>Hoge merge-commit ratio</value>
</data>
<data name="Report_MergeRatioDesc" xml:space="preserve">
<value>Uw repository heeft een {0}% merge-commit ratio ({1}/{2} commits). Overweeg een rebase-workflow of squash merges te gebruiken.</value>
</data>
<data name="Report_MergeFixCommits" xml:space="preserve">
<value>Merge-fix commits gedetecteerd</value>
</data>
<data name="Report_MergeFixDesc" xml:space="preserve">
<value>{0} commits gevonden met berichten zoals 'fix merge' gedetecteerd na merges.</value>
</data>
<data name="Report_CrossMerges" xml:space="preserve">
<value>Cross-merges tussen branches</value>
</data>
<data name="Report_CrossMergesDesc" xml:space="preserve">
<value>{0} cross-merges tussen feature branches gedetecteerd. Gebruik feature branches die alleen naar main mergen.</value>
</data>
<data name="Report_StaleBranches" xml:space="preserve">
<value>Verouderde branches</value>
</data>
<data name="Report_StaleBranchesDesc" xml:space="preserve">
<value>{0} branches gevonden zonder activiteit in 30+ dagen.</value>
</data>
<!-- ==================== Safety Warnings ==================== -->
<data name="Safety_UncommittedChanges" xml:space="preserve">
<value>U heeft niet-gecommitte wijzigingen. Commit of stash ze eerst.</value>
</data>
<data name="Safety_PushedCommits" xml:space="preserve">
<value>{0} commit(s) zijn al gepusht naar de remote. Het herschrijven ervan vereist een force push en kan andere medewerkers beïnvloeden.</value>
</data>
<data name="Safety_BehindRemote" xml:space="preserve">
<value>Uw branch loopt {0} commit(s) achter op de remote. Overweeg eerst te pullen om conflicten te voorkomen.</value>
</data>
<!-- ==================== Health Status ==================== -->
<data name="HealthStatus_NeedsAttention" xml:space="preserve">
<value>Repository heeft merkbare problemen die moeten worden aangepakt.</value>
</data>
<data name="HealthStatus_Critical" xml:space="preserve">
<value>Repository vereist onmiddellijke aandacht. Geschiedenis is ernstig gedegradeerd.</value>
</data>
</root>

View File

@@ -0,0 +1,421 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="0" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- ==================== Commit Message Analyzer ==================== -->
<data name="Analyzer_MessageEmpty" xml:space="preserve">
<value>Mensagem de commit está vazia</value>
</data>
<data name="Analyzer_SubjectTooShort" xml:space="preserve">
<value>Assunto tem {0} caracteres, mínimo é {1}</value>
</data>
<data name="Analyzer_SubjectTooLong" xml:space="preserve">
<value>Assunto tem {0} caracteres, máximo recomendado é {1}</value>
</data>
<data name="Analyzer_BannedPhrase" xml:space="preserve">
<value>Assunto usa frase não descritiva: '{0}'</value>
</data>
<data name="Analyzer_NotConventional" xml:space="preserve">
<value>Mensagem não segue o formato de commit convencional (tipo: assunto)</value>
</data>
<data name="Analyzer_UnknownType" xml:space="preserve">
<value>Tipo de commit convencional desconhecido: {0}</value>
</data>
<data name="Analyzer_NoIssueRef" xml:space="preserve">
<value>Nenhuma referência a issue encontrada (ex: #123 ou JIRA-123)</value>
</data>
<data name="Analyzer_CapitalLetter" xml:space="preserve">
<value>Assunto deve começar com letra maiúscula</value>
</data>
<data name="Analyzer_NoPeriod" xml:space="preserve">
<value>Assunto não deve terminar com ponto final</value>
</data>
<data name="Analyzer_ImperativeMood" xml:space="preserve">
<value>Use modo imperativo: '{0}' → '{1}' (ex: 'Add' não 'Added')</value>
</data>
<data name="Analyzer_BodyTooShort" xml:space="preserve">
<value>Corpo tem {0} caracteres, mínimo é {1}</value>
</data>
<data name="Analyzer_BlankLine" xml:space="preserve">
<value>Adicione uma linha em branco entre o assunto e o corpo</value>
</data>
<data name="Analyzer_NotDescriptive" xml:space="preserve">
<value>'{0}' não descreve o que mudou em {1} arquivos</value>
</data>
<data name="Analyzer_TooVague" xml:space="preserve">
<value>Mensagem muito vaga para {0} arquivos alterados - descreva O QUE mudou</value>
</data>
<data name="Analyzer_LargeChange" xml:space="preserve">
<value>Mudança grande ({0} arquivos, {1} linhas) merece uma mensagem mais descritiva</value>
</data>
<data name="Analyzer_MajorChange" xml:space="preserve">
<value>Mudança importante ({0} arquivos) deve incluir um corpo explicando o porquê</value>
</data>
<data name="Analyzer_MentionArea" xml:space="preserve">
<value>Considere mencionar qual área mudou (arquivos: {0})</value>
</data>
<!-- ==================== Git Operations Service ==================== -->
<data name="Git_NoCommits" xml:space="preserve">
<value>Nenhum commit no repositório</value>
</data>
<data name="Git_CommitNotFound" xml:space="preserve">
<value>Commit não encontrado: {0}</value>
</data>
<data name="Git_NotAncestor" xml:space="preserve">
<value>Commit de destino não é um ancestral de HEAD</value>
</data>
<data name="Git_NoTargetCommits" xml:space="preserve">
<value>Não foi possível encontrar commits de destino no repositório</value>
</data>
<data name="Git_ParentMismatch" xml:space="preserve">
<value>Falha na criação do commit: incompatibilidade de pai para o commit {0}</value>
</data>
<data name="Git_HeadUpdateFailed" xml:space="preserve">
<value>Falha ao atualizar HEAD para o novo commit {0}</value>
</data>
<data name="Git_VerificationFailed" xml:space="preserve">
<value>Falha na verificação do disco: HEAD deveria ser {0} mas é {1}</value>
</data>
<data name="Git_OldCommitReachable" xml:space="preserve">
<value>Commit antigo {0} ainda acessível a partir de HEAD após reescrita</value>
</data>
<data name="Git_Error" xml:space="preserve">
<value>Erro do Git: {0}</value>
</data>
<data name="Git_RemoteNotFound" xml:space="preserve">
<value>Remoto '{0}' não encontrado</value>
</data>
<data name="Git_NoUpstreamNoOrigin" xml:space="preserve">
<value>Nenhum branch upstream configurado e nenhum remoto 'origin' encontrado.
Configure o rastreamento manualmente com: git push -u origin {0}</value>
</data>
<data name="Git_ForcePushSuccess" xml:space="preserve">
<value>Force push bem-sucedido</value>
</data>
<data name="Git_ForcePushedTo" xml:space="preserve">
<value>Force push realizado para origin/{0}</value>
</data>
<data name="Git_ProcessFailed" xml:space="preserve">
<value>Falha ao iniciar o processo git</value>
</data>
<data name="Git_ForcePushSuccessCmd" xml:space="preserve">
<value>Force push bem-sucedido (via comando git)</value>
</data>
<data name="Git_PushFailed" xml:space="preserve">
<value>Push falhou: {0}</value>
</data>
<data name="Git_CommandFailed" xml:space="preserve">
<value>Falha ao executar comando git: {0}</value>
</data>
<data name="Git_NoUpstream" xml:space="preserve">
<value>Nenhum branch upstream configurado. Configure o rastreamento com: git push -u origin &lt;branch&gt;</value>
</data>
<data name="Git_NonFastForward" xml:space="preserve">
<value>Push rejeitado: non-fast-forward. Faça pull das mudanças primeiro ou use force push.</value>
</data>
<data name="Git_PushSuccessCmd" xml:space="preserve">
<value>Push bem-sucedido (via comando git)</value>
</data>
<!-- ==================== Cleanup Executor ==================== -->
<data name="Cleanup_PushedCommitsBlocked" xml:space="preserve">
<value>Alguns commits já foram enviados. Habilite 'AllowPushedCommits' para prosseguir.</value>
</data>
<data name="Cleanup_NotImplemented" xml:space="preserve">
<value>Tipo de limpeza '{0}' ainda não foi implementado</value>
</data>
<data name="Cleanup_Rebuilding" xml:space="preserve">
<value>Reconstruindo histórico de commits...</value>
</data>
<data name="Cleanup_RebuildingCount" xml:space="preserve">
<value>Reconstruindo {0} commits...</value>
</data>
<data name="Cleanup_ProcessingCommit" xml:space="preserve">
<value>Processando commit {0}/{1}...</value>
</data>
<data name="Cleanup_UpdatingBranch" xml:space="preserve">
<value>Atualizando referência do branch...</value>
</data>
<data name="Cleanup_SquashingMerges" xml:space="preserve">
<value>Consolidando commits de merge...</value>
</data>
<data name="Cleanup_DropDuplicatesFailed" xml:space="preserve">
<value>Falha ao remover commits duplicados: {0}</value>
</data>
<data name="Cleanup_NeedTwoCommits" xml:space="preserve">
<value>São necessários pelo menos 2 commits para consolidar</value>
</data>
<data name="Cleanup_NoCommitsOnBranch" xml:space="preserve">
<value>Nenhum commit encontrado no branch atual</value>
</data>
<data name="Cleanup_NoMatchingCommits" xml:space="preserve">
<value>Nenhum commit correspondente encontrado para remover</value>
</data>
<data name="Cleanup_NoCommitsToSquash" xml:space="preserve">
<value>Nenhum commit especificado para consolidar</value>
</data>
<data name="Cleanup_NoMergeCommits" xml:space="preserve">
<value>Nenhum commit de merge correspondente encontrado para consolidar</value>
</data>
<data name="Cleanup_SquashMergeFailed" xml:space="preserve">
<value>Falha ao consolidar commits de merge: {0}</value>
</data>
<data name="Cleanup_NoCommitsToFix" xml:space="preserve">
<value>Nenhum commit especificado para corrigir</value>
</data>
<data name="Cleanup_FixAuthorFailed" xml:space="preserve">
<value>Falha ao corrigir autoria: {0}</value>
</data>
<data name="Cleanup_ConsolidatingFixes" xml:space="preserve">
<value>Consolidando commits de correção de merge...</value>
</data>
<data name="Cleanup_NoFixCommits" xml:space="preserve">
<value>Nenhum commit de correção para consolidar</value>
</data>
<data name="Cleanup_NoMatchingFixes" xml:space="preserve">
<value>Nenhum commit de correção correspondente encontrado para consolidar</value>
</data>
<data name="Cleanup_ConsolidateFailed" xml:space="preserve">
<value>Falha ao consolidar commits de correção de merge: {0}</value>
</data>
<data name="Cleanup_ArchivingBranches" xml:space="preserve">
<value>Arquivando branches obsoletos...</value>
</data>
<data name="Cleanup_ProcessingBranch" xml:space="preserve">
<value>Processando branch {0}...</value>
</data>
<data name="Cleanup_ArchiveComplete" xml:space="preserve">
<value>Arquivamento concluído</value>
</data>
<data name="Cleanup_ArchiveFailed" xml:space="preserve">
<value>Falha ao arquivar branches: {0}</value>
</data>
<data name="Cleanup_AnalyzingStructure" xml:space="preserve">
<value>Analisando estrutura do branch...</value>
</data>
<data name="Cleanup_FoundCommits" xml:space="preserve">
<value>Encontrados {0} commits para linearizar...</value>
</data>
<data name="Cleanup_Linearizing" xml:space="preserve">
<value>Linearizando {0} commits (removendo {1} merges)...</value>
</data>
<data name="Cleanup_RebuildingCommit" xml:space="preserve">
<value>Reconstruindo commit {0}/{1}...</value>
</data>
<data name="Cleanup_Reconciling" xml:space="preserve">
<value>Reconciliando estado final...</value>
</data>
<data name="Cleanup_AlreadyLinear" xml:space="preserve">
<value>Histórico já é linear - nenhum commit de merge encontrado</value>
</data>
<data name="Cleanup_LinearizeComplete" xml:space="preserve">
<value>Linearização concluída</value>
</data>
<data name="Cleanup_LinearizeFailed" xml:space="preserve">
<value>Falha ao linearizar histórico: {0}</value>
</data>
<data name="Cleanup_DescReword" xml:space="preserve">
<value>Irá reescrever {0} mensagem(ns) de commit para melhorar a qualidade.</value>
</data>
<data name="Cleanup_DescSquash" xml:space="preserve">
<value>Irá consolidar {0} commits duplicados em 1.</value>
</data>
<data name="Cleanup_DescConsolidate" xml:space="preserve">
<value>Irá consolidar {0} commits de correção de merge.</value>
</data>
<data name="Cleanup_DescAuthorship" xml:space="preserve">
<value>Irá corrigir autoria em {0} commit(s).</value>
</data>
<data name="Cleanup_DescTrivialMerges" xml:space="preserve">
<value>Irá consolidar {0} merges triviais.</value>
</data>
<data name="Cleanup_DescArchive" xml:space="preserve">
<value>Irá arquivar branches obsoletos (excluir se mesclado, marcar com tag caso contrário).</value>
</data>
<data name="Cleanup_DescLinearize" xml:space="preserve">
<value>Irá linearizar histórico removendo commits de merge e ordenando por data.</value>
</data>
<data name="Cleanup_DescGeneric" xml:space="preserve">
<value>Irá processar {0} commit(s).</value>
</data>
<data name="Cleanup_ReconcileMerge" xml:space="preserve">
<value>Reconciliar: mesclar estado final após linearização</value>
</data>
<!-- ==================== Validation ==================== -->
<data name="Validation_WorkspaceRequired" xml:space="preserve">
<value>WorkspaceRoot é obrigatório</value>
</data>
<data name="Validation_WorkspaceNotFound" xml:space="preserve">
<value>Diretório WorkspaceRoot não existe: {0}</value>
</data>
<data name="Validation_MaxCommitsPositive" xml:space="preserve">
<value>MaxCommitsPerRepo deve ser maior que 0</value>
</data>
<data name="Validation_RulesNull" xml:space="preserve">
<value>Rules não pode ser nulo</value>
</data>
<data name="Validation_AiOptionsNull" xml:space="preserve">
<value>Opções de Ai não podem ser nulas</value>
</data>
<data name="Validation_InvalidOptions" xml:space="preserve">
<value>GitImproverOptions inválido: {0}</value>
</data>
<data name="Validation_WeightsSum" xml:space="preserve">
<value>Pesos devem somar 1.0 (atual: {0})</value>
</data>
<!-- ==================== Service Messages ==================== -->
<data name="Service_UnknownError" xml:space="preserve">
<value>Erro desconhecido</value>
</data>
<data name="Service_RepoNotRegistered" xml:space="preserve">
<value>Repositório não registrado</value>
</data>
<data name="Service_UncommittedChanges" xml:space="preserve">
<value>Não é possível reescrever commits com alterações não confirmadas. Por favor, faça commit ou stash das suas alterações primeiro.</value>
</data>
<data name="Service_RepoNotFound" xml:space="preserve">
<value>Repositório não encontrado: {0}</value>
</data>
<data name="Service_NoSuggestion" xml:space="preserve">
<value>Nenhuma mensagem sugerida disponível</value>
</data>
<data name="Service_RepoNotRegisteredPath" xml:space="preserve">
<value>Repositório não registrado: {0}</value>
</data>
<data name="Service_ApiKeyNotConfigured" xml:space="preserve">
<value>Chave de API não está configurada. Por favor, defina sua chave de API nas Configurações.</value>
</data>
<data name="Service_AiAnalysisFailed" xml:space="preserve">
<value>Falha na análise de IA</value>
</data>
<data name="Service_AiFallback" xml:space="preserve">
<value>IA não retornou saída estruturada - revertido para mensagem original</value>
</data>
<data name="Service_PushSuccess" xml:space="preserve">
<value>Push bem-sucedido</value>
</data>
<!-- ==================== Health Analyzer Status ==================== -->
<data name="Health_LoadingCommits" xml:space="preserve">
<value>Carregando commits</value>
</data>
<data name="Health_DetectingDuplicates" xml:space="preserve">
<value>Detectando duplicatas</value>
</data>
<data name="Health_AnalyzingMerges" xml:space="preserve">
<value>Analisando commits de merge</value>
</data>
<data name="Health_AnalyzingBranches" xml:space="preserve">
<value>Analisando complexidade dos branches</value>
</data>
<data name="Health_AnalyzingMessages" xml:space="preserve">
<value>Analisando qualidade das mensagens</value>
</data>
<data name="Health_AnalyzingAuthorship" xml:space="preserve">
<value>Analisando autoria</value>
</data>
<data name="Health_Complete" xml:space="preserve">
<value>Concluído</value>
</data>
<!-- ==================== Health Report Issues ==================== -->
<data name="Report_DuplicateContent" xml:space="preserve">
<value>Commits duplicados com conteúdo idêntico</value>
</data>
<data name="Report_DuplicateContentDesc" xml:space="preserve">
<value>Encontrados {0} grupos de commits com conteúdo de arquivo idêntico ({1} commits redundantes). É seguro consolidá-los pois têm o mesmo SHA de árvore.</value>
</data>
<data name="Report_DuplicateMessages" xml:space="preserve">
<value>Commits com mensagens duplicadas</value>
</data>
<data name="Report_DuplicateMessagesDesc" xml:space="preserve">
<value>Encontrados {0} grupos de commits com mensagens idênticas mas alterações de código diferentes ({1} commits). Considere usar mensagens mais descritivas para diferenciar as mudanças.</value>
</data>
<data name="Report_ExcessiveMerges" xml:space="preserve">
<value>Commits de merge excessivos</value>
</data>
<data name="Report_HighMergeRatio" xml:space="preserve">
<value>Alta proporção de commits de merge</value>
</data>
<data name="Report_MergeRatioDesc" xml:space="preserve">
<value>Seu repositório tem uma proporção de {0}% de commits de merge ({1}/{2} commits). Considere usar fluxo de trabalho com rebase ou squash merges.</value>
</data>
<data name="Report_MergeFixCommits" xml:space="preserve">
<value>Commits de correção de merge detectados</value>
</data>
<data name="Report_MergeFixDesc" xml:space="preserve">
<value>Encontrados {0} commits com mensagens como 'fix merge' detectados após merges.</value>
</data>
<data name="Report_CrossMerges" xml:space="preserve">
<value>Cross-merges entre branches</value>
</data>
<data name="Report_CrossMergesDesc" xml:space="preserve">
<value>Detectados {0} cross-merges entre branches de funcionalidade. Use branches de funcionalidade que mesclam apenas para main.</value>
</data>
<data name="Report_StaleBranches" xml:space="preserve">
<value>Branches obsoletos</value>
</data>
<data name="Report_StaleBranchesDesc" xml:space="preserve">
<value>Encontrados {0} branches sem atividade em mais de 30 dias.</value>
</data>
<!-- ==================== Safety Warnings ==================== -->
<data name="Safety_UncommittedChanges" xml:space="preserve">
<value>Você tem alterações não confirmadas. Por favor, faça commit ou stash delas primeiro.</value>
</data>
<data name="Safety_PushedCommits" xml:space="preserve">
<value>{0} commit(s) já foram enviados para o remoto. Reescrevê-los exigirá um force push e pode afetar colaboradores.</value>
</data>
<data name="Safety_BehindRemote" xml:space="preserve">
<value>Seu branch está {0} commit(s) atrás do remoto. Considere fazer pull primeiro para evitar conflitos.</value>
</data>
<!-- ==================== Health Status ==================== -->
<data name="HealthStatus_NeedsAttention" xml:space="preserve">
<value>Repositório tem problemas notáveis que devem ser abordados.</value>
</data>
<data name="HealthStatus_Critical" xml:space="preserve">
<value>Repositório requer atenção imediata. Histórico está severamente degradado.</value>
</data>
</root>

View File

@@ -0,0 +1,421 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="0" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- ==================== Commit Message Analyzer ==================== -->
<data name="Analyzer_MessageEmpty" xml:space="preserve">
<value>Commit message is empty</value>
</data>
<data name="Analyzer_SubjectTooShort" xml:space="preserve">
<value>Subject is {0} chars, minimum is {1}</value>
</data>
<data name="Analyzer_SubjectTooLong" xml:space="preserve">
<value>Subject is {0} chars, recommended max is {1}</value>
</data>
<data name="Analyzer_BannedPhrase" xml:space="preserve">
<value>Subject uses non-descriptive phrase: '{0}'</value>
</data>
<data name="Analyzer_NotConventional" xml:space="preserve">
<value>Message doesn't follow conventional commit format (type: subject)</value>
</data>
<data name="Analyzer_UnknownType" xml:space="preserve">
<value>Unknown conventional commit type: {0}</value>
</data>
<data name="Analyzer_NoIssueRef" xml:space="preserve">
<value>No issue reference found (e.g., #123 or JIRA-123)</value>
</data>
<data name="Analyzer_CapitalLetter" xml:space="preserve">
<value>Subject should start with a capital letter</value>
</data>
<data name="Analyzer_NoPeriod" xml:space="preserve">
<value>Subject should not end with a period</value>
</data>
<data name="Analyzer_ImperativeMood" xml:space="preserve">
<value>Use imperative mood: '{0}' → '{1}' (e.g., 'Add' not 'Added')</value>
</data>
<data name="Analyzer_BodyTooShort" xml:space="preserve">
<value>Body is {0} chars, minimum is {1}</value>
</data>
<data name="Analyzer_BlankLine" xml:space="preserve">
<value>Add a blank line between subject and body</value>
</data>
<data name="Analyzer_NotDescriptive" xml:space="preserve">
<value>'{0}' doesn't describe what changed in {1} files</value>
</data>
<data name="Analyzer_TooVague" xml:space="preserve">
<value>Message is too vague for {0} changed files - describe WHAT changed</value>
</data>
<data name="Analyzer_LargeChange" xml:space="preserve">
<value>Large change ({0} files, {1} lines) deserves a more descriptive message</value>
</data>
<data name="Analyzer_MajorChange" xml:space="preserve">
<value>Major change ({0} files) should include a body explaining why</value>
</data>
<data name="Analyzer_MentionArea" xml:space="preserve">
<value>Consider mentioning what area changed (files: {0})</value>
</data>
<!-- ==================== Git Operations Service ==================== -->
<data name="Git_NoCommits" xml:space="preserve">
<value>No commits in repository</value>
</data>
<data name="Git_CommitNotFound" xml:space="preserve">
<value>Commit not found: {0}</value>
</data>
<data name="Git_NotAncestor" xml:space="preserve">
<value>Target commit is not an ancestor of HEAD</value>
</data>
<data name="Git_NoTargetCommits" xml:space="preserve">
<value>Could not find any target commits in repository</value>
</data>
<data name="Git_ParentMismatch" xml:space="preserve">
<value>Commit creation failed: parent mismatch for commit {0}</value>
</data>
<data name="Git_HeadUpdateFailed" xml:space="preserve">
<value>Failed to update HEAD to new commit {0}</value>
</data>
<data name="Git_VerificationFailed" xml:space="preserve">
<value>Disk verification failed: HEAD should be {0} but is {1}</value>
</data>
<data name="Git_OldCommitReachable" xml:space="preserve">
<value>Old commit {0} still reachable from HEAD after rewrite</value>
</data>
<data name="Git_Error" xml:space="preserve">
<value>Git error: {0}</value>
</data>
<data name="Git_RemoteNotFound" xml:space="preserve">
<value>Remote '{0}' not found</value>
</data>
<data name="Git_NoUpstreamNoOrigin" xml:space="preserve">
<value>No upstream branch configured and no 'origin' remote found.
Set tracking manually with: git push -u origin {0}</value>
</data>
<data name="Git_ForcePushSuccess" xml:space="preserve">
<value>Force push successful</value>
</data>
<data name="Git_ForcePushedTo" xml:space="preserve">
<value>Force pushed to origin/{0}</value>
</data>
<data name="Git_ProcessFailed" xml:space="preserve">
<value>Failed to start git process</value>
</data>
<data name="Git_ForcePushSuccessCmd" xml:space="preserve">
<value>Force push successful (via git command)</value>
</data>
<data name="Git_PushFailed" xml:space="preserve">
<value>Push failed: {0}</value>
</data>
<data name="Git_CommandFailed" xml:space="preserve">
<value>Failed to run git command: {0}</value>
</data>
<data name="Git_NoUpstream" xml:space="preserve">
<value>No upstream branch configured. Set tracking with: git push -u origin &lt;branch&gt;</value>
</data>
<data name="Git_NonFastForward" xml:space="preserve">
<value>Push rejected: non-fast-forward. Pull changes first or use force push.</value>
</data>
<data name="Git_PushSuccessCmd" xml:space="preserve">
<value>Push successful (via git command)</value>
</data>
<!-- ==================== Cleanup Executor ==================== -->
<data name="Cleanup_PushedCommitsBlocked" xml:space="preserve">
<value>Some commits have been pushed. Enable 'AllowPushedCommits' to proceed.</value>
</data>
<data name="Cleanup_NotImplemented" xml:space="preserve">
<value>Cleanup type '{0}' is not yet implemented</value>
</data>
<data name="Cleanup_Rebuilding" xml:space="preserve">
<value>Rebuilding commit history...</value>
</data>
<data name="Cleanup_RebuildingCount" xml:space="preserve">
<value>Rebuilding {0} commits...</value>
</data>
<data name="Cleanup_ProcessingCommit" xml:space="preserve">
<value>Processing commit {0}/{1}...</value>
</data>
<data name="Cleanup_UpdatingBranch" xml:space="preserve">
<value>Updating branch reference...</value>
</data>
<data name="Cleanup_SquashingMerges" xml:space="preserve">
<value>Squashing merge commits...</value>
</data>
<data name="Cleanup_DropDuplicatesFailed" xml:space="preserve">
<value>Failed to drop duplicate commits: {0}</value>
</data>
<data name="Cleanup_NeedTwoCommits" xml:space="preserve">
<value>Need at least 2 commits to squash</value>
</data>
<data name="Cleanup_NoCommitsOnBranch" xml:space="preserve">
<value>No commits found on current branch</value>
</data>
<data name="Cleanup_NoMatchingCommits" xml:space="preserve">
<value>No matching commits found to drop</value>
</data>
<data name="Cleanup_NoCommitsToSquash" xml:space="preserve">
<value>No commits specified to squash</value>
</data>
<data name="Cleanup_NoMergeCommits" xml:space="preserve">
<value>No matching merge commits found to squash</value>
</data>
<data name="Cleanup_SquashMergeFailed" xml:space="preserve">
<value>Failed to squash merge commits: {0}</value>
</data>
<data name="Cleanup_NoCommitsToFix" xml:space="preserve">
<value>No commits specified to fix</value>
</data>
<data name="Cleanup_FixAuthorFailed" xml:space="preserve">
<value>Failed to fix authorship: {0}</value>
</data>
<data name="Cleanup_ConsolidatingFixes" xml:space="preserve">
<value>Consolidating merge fix commits...</value>
</data>
<data name="Cleanup_NoFixCommits" xml:space="preserve">
<value>No fix commits to consolidate</value>
</data>
<data name="Cleanup_NoMatchingFixes" xml:space="preserve">
<value>No matching fix commits found to consolidate</value>
</data>
<data name="Cleanup_ConsolidateFailed" xml:space="preserve">
<value>Failed to consolidate merge fix commits: {0}</value>
</data>
<data name="Cleanup_ArchivingBranches" xml:space="preserve">
<value>Archiving stale branches...</value>
</data>
<data name="Cleanup_ProcessingBranch" xml:space="preserve">
<value>Processing branch {0}...</value>
</data>
<data name="Cleanup_ArchiveComplete" xml:space="preserve">
<value>Archive complete</value>
</data>
<data name="Cleanup_ArchiveFailed" xml:space="preserve">
<value>Failed to archive branches: {0}</value>
</data>
<data name="Cleanup_AnalyzingStructure" xml:space="preserve">
<value>Analyzing branch structure...</value>
</data>
<data name="Cleanup_FoundCommits" xml:space="preserve">
<value>Found {0} commits to linearize...</value>
</data>
<data name="Cleanup_Linearizing" xml:space="preserve">
<value>Linearizing {0} commits (removing {1} merges)...</value>
</data>
<data name="Cleanup_RebuildingCommit" xml:space="preserve">
<value>Rebuilding commit {0}/{1}...</value>
</data>
<data name="Cleanup_Reconciling" xml:space="preserve">
<value>Reconciling final state...</value>
</data>
<data name="Cleanup_AlreadyLinear" xml:space="preserve">
<value>History is already linear - no merge commits found</value>
</data>
<data name="Cleanup_LinearizeComplete" xml:space="preserve">
<value>Linearization complete</value>
</data>
<data name="Cleanup_LinearizeFailed" xml:space="preserve">
<value>Failed to linearize history: {0}</value>
</data>
<data name="Cleanup_DescReword" xml:space="preserve">
<value>Will reword {0} commit message(s) to improve quality.</value>
</data>
<data name="Cleanup_DescSquash" xml:space="preserve">
<value>Will squash {0} duplicate commits into 1.</value>
</data>
<data name="Cleanup_DescConsolidate" xml:space="preserve">
<value>Will consolidate {0} merge-fix commits.</value>
</data>
<data name="Cleanup_DescAuthorship" xml:space="preserve">
<value>Will fix authorship on {0} commit(s).</value>
</data>
<data name="Cleanup_DescTrivialMerges" xml:space="preserve">
<value>Will consolidate {0} trivial merges.</value>
</data>
<data name="Cleanup_DescArchive" xml:space="preserve">
<value>Will archive stale branches (delete if merged, tag otherwise).</value>
</data>
<data name="Cleanup_DescLinearize" xml:space="preserve">
<value>Will linearize history by removing merge commits and sorting by date.</value>
</data>
<data name="Cleanup_DescGeneric" xml:space="preserve">
<value>Will process {0} commit(s).</value>
</data>
<data name="Cleanup_ReconcileMerge" xml:space="preserve">
<value>Reconcile: merge final state after linearization</value>
</data>
<!-- ==================== Validation ==================== -->
<data name="Validation_WorkspaceRequired" xml:space="preserve">
<value>WorkspaceRoot is required</value>
</data>
<data name="Validation_WorkspaceNotFound" xml:space="preserve">
<value>WorkspaceRoot directory does not exist: {0}</value>
</data>
<data name="Validation_MaxCommitsPositive" xml:space="preserve">
<value>MaxCommitsPerRepo must be greater than 0</value>
</data>
<data name="Validation_RulesNull" xml:space="preserve">
<value>Rules cannot be null</value>
</data>
<data name="Validation_AiOptionsNull" xml:space="preserve">
<value>Ai options cannot be null</value>
</data>
<data name="Validation_InvalidOptions" xml:space="preserve">
<value>Invalid GitImproverOptions: {0}</value>
</data>
<data name="Validation_WeightsSum" xml:space="preserve">
<value>Weights must sum to 1.0 (current: {0})</value>
</data>
<!-- ==================== Service Messages ==================== -->
<data name="Service_UnknownError" xml:space="preserve">
<value>Unknown error</value>
</data>
<data name="Service_RepoNotRegistered" xml:space="preserve">
<value>Repository not registered</value>
</data>
<data name="Service_UncommittedChanges" xml:space="preserve">
<value>Cannot rewrite commits with uncommitted changes. Please commit or stash your changes first.</value>
</data>
<data name="Service_RepoNotFound" xml:space="preserve">
<value>Repository not found: {0}</value>
</data>
<data name="Service_NoSuggestion" xml:space="preserve">
<value>No suggested message available</value>
</data>
<data name="Service_RepoNotRegisteredPath" xml:space="preserve">
<value>Repository not registered: {0}</value>
</data>
<data name="Service_ApiKeyNotConfigured" xml:space="preserve">
<value>API key is not configured. Please set your API key in Settings.</value>
</data>
<data name="Service_AiAnalysisFailed" xml:space="preserve">
<value>AI analysis failed</value>
</data>
<data name="Service_AiFallback" xml:space="preserve">
<value>AI did not return structured output - fell back to original message</value>
</data>
<data name="Service_PushSuccess" xml:space="preserve">
<value>Push successful</value>
</data>
<!-- ==================== Health Analyzer Status ==================== -->
<data name="Health_LoadingCommits" xml:space="preserve">
<value>Loading commits</value>
</data>
<data name="Health_DetectingDuplicates" xml:space="preserve">
<value>Detecting duplicates</value>
</data>
<data name="Health_AnalyzingMerges" xml:space="preserve">
<value>Analyzing merge commits</value>
</data>
<data name="Health_AnalyzingBranches" xml:space="preserve">
<value>Analyzing branch complexity</value>
</data>
<data name="Health_AnalyzingMessages" xml:space="preserve">
<value>Analyzing message quality</value>
</data>
<data name="Health_AnalyzingAuthorship" xml:space="preserve">
<value>Analyzing authorship</value>
</data>
<data name="Health_Complete" xml:space="preserve">
<value>Complete</value>
</data>
<!-- ==================== Health Report Issues ==================== -->
<data name="Report_DuplicateContent" xml:space="preserve">
<value>Duplicate commits with identical content</value>
</data>
<data name="Report_DuplicateContentDesc" xml:space="preserve">
<value>Found {0} groups of commits with identical file content ({1} redundant commits). These are safe to squash as they have the same tree SHA.</value>
</data>
<data name="Report_DuplicateMessages" xml:space="preserve">
<value>Commits with duplicate messages</value>
</data>
<data name="Report_DuplicateMessagesDesc" xml:space="preserve">
<value>Found {0} groups of commits with identical messages but different code changes ({1} commits). Consider using more descriptive messages to differentiate changes.</value>
</data>
<data name="Report_ExcessiveMerges" xml:space="preserve">
<value>Excessive merge commits</value>
</data>
<data name="Report_HighMergeRatio" xml:space="preserve">
<value>High merge commit ratio</value>
</data>
<data name="Report_MergeRatioDesc" xml:space="preserve">
<value>Your repository has a {0}% merge commit ratio ({1}/{2} commits). Consider using rebase workflow or squash merges.</value>
</data>
<data name="Report_MergeFixCommits" xml:space="preserve">
<value>Merge fix commits detected</value>
</data>
<data name="Report_MergeFixDesc" xml:space="preserve">
<value>Found {0} commits with messages like 'fix merge' detected after merges.</value>
</data>
<data name="Report_CrossMerges" xml:space="preserve">
<value>Cross-merges between branches</value>
</data>
<data name="Report_CrossMergesDesc" xml:space="preserve">
<value>Detected {0} cross-merges between feature branches. Use feature branches that only merge into main.</value>
</data>
<data name="Report_StaleBranches" xml:space="preserve">
<value>Stale branches</value>
</data>
<data name="Report_StaleBranchesDesc" xml:space="preserve">
<value>Found {0} branches with no activity in 30+ days.</value>
</data>
<!-- ==================== Safety Warnings ==================== -->
<data name="Safety_UncommittedChanges" xml:space="preserve">
<value>You have uncommitted changes. Please commit or stash them first.</value>
</data>
<data name="Safety_PushedCommits" xml:space="preserve">
<value>{0} commit(s) have already been pushed to the remote. Rewriting them will require a force push and may affect collaborators.</value>
</data>
<data name="Safety_BehindRemote" xml:space="preserve">
<value>Your branch is {0} commit(s) behind the remote. Consider pulling first to avoid conflicts.</value>
</data>
<!-- ==================== Health Status ==================== -->
<data name="HealthStatus_NeedsAttention" xml:space="preserve">
<value>Repository has noticeable issues that should be addressed.</value>
</data>
<data name="HealthStatus_Critical" xml:space="preserve">
<value>Repository requires immediate attention. History is severely degraded.</value>
</data>
</root>

View File

@@ -0,0 +1,421 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="0" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- ==================== Commit Message Analyzer ==================== -->
<data name="Analyzer_MessageEmpty" xml:space="preserve">
<value>Сообщение commit пустое</value>
</data>
<data name="Analyzer_SubjectTooShort" xml:space="preserve">
<value>Заголовок содержит {0} символов, минимум {1}</value>
</data>
<data name="Analyzer_SubjectTooLong" xml:space="preserve">
<value>Заголовок содержит {0} символов, рекомендуемый максимум {1}</value>
</data>
<data name="Analyzer_BannedPhrase" xml:space="preserve">
<value>Заголовок использует неинформативную фразу: '{0}'</value>
</data>
<data name="Analyzer_NotConventional" xml:space="preserve">
<value>Сообщение не соответствует формату conventional commit (тип: описание)</value>
</data>
<data name="Analyzer_UnknownType" xml:space="preserve">
<value>Неизвестный тип conventional commit: {0}</value>
</data>
<data name="Analyzer_NoIssueRef" xml:space="preserve">
<value>Не найдена ссылка на задачу (например, #123 или JIRA-123)</value>
</data>
<data name="Analyzer_CapitalLetter" xml:space="preserve">
<value>Заголовок должен начинаться с заглавной буквы</value>
</data>
<data name="Analyzer_NoPeriod" xml:space="preserve">
<value>Заголовок не должен заканчиваться точкой</value>
</data>
<data name="Analyzer_ImperativeMood" xml:space="preserve">
<value>Используйте повелительное наклонение: '{0}' → '{1}' (например, 'Add' а не 'Added')</value>
</data>
<data name="Analyzer_BodyTooShort" xml:space="preserve">
<value>Тело содержит {0} символов, минимум {1}</value>
</data>
<data name="Analyzer_BlankLine" xml:space="preserve">
<value>Добавьте пустую строку между заголовком и телом</value>
</data>
<data name="Analyzer_NotDescriptive" xml:space="preserve">
<value>'{0}' не описывает изменения в {1} файлах</value>
</data>
<data name="Analyzer_TooVague" xml:space="preserve">
<value>Сообщение слишком расплывчатое для {0} измененных файлов - опишите ЧТО изменилось</value>
</data>
<data name="Analyzer_LargeChange" xml:space="preserve">
<value>Большое изменение ({0} файлов, {1} строк) заслуживает более подробного описания</value>
</data>
<data name="Analyzer_MajorChange" xml:space="preserve">
<value>Значительное изменение ({0} файлов) должно включать тело с объяснением причин</value>
</data>
<data name="Analyzer_MentionArea" xml:space="preserve">
<value>Рассмотрите возможность указать, какая область изменилась (файлы: {0})</value>
</data>
<!-- ==================== Git Operations Service ==================== -->
<data name="Git_NoCommits" xml:space="preserve">
<value>В репозитории нет commit</value>
</data>
<data name="Git_CommitNotFound" xml:space="preserve">
<value>Commit не найден: {0}</value>
</data>
<data name="Git_NotAncestor" xml:space="preserve">
<value>Целевой commit не является предком HEAD</value>
</data>
<data name="Git_NoTargetCommits" xml:space="preserve">
<value>Не удалось найти целевые commit в репозитории</value>
</data>
<data name="Git_ParentMismatch" xml:space="preserve">
<value>Не удалось создать commit: несоответствие родителя для commit {0}</value>
</data>
<data name="Git_HeadUpdateFailed" xml:space="preserve">
<value>Не удалось обновить HEAD на новый commit {0}</value>
</data>
<data name="Git_VerificationFailed" xml:space="preserve">
<value>Не удалось выполнить проверку на диске: HEAD должен быть {0}, но является {1}</value>
</data>
<data name="Git_OldCommitReachable" xml:space="preserve">
<value>Старый commit {0} все еще доступен из HEAD после перезаписи</value>
</data>
<data name="Git_Error" xml:space="preserve">
<value>Ошибка Git: {0}</value>
</data>
<data name="Git_RemoteNotFound" xml:space="preserve">
<value>Удаленный репозиторий '{0}' не найден</value>
</data>
<data name="Git_NoUpstreamNoOrigin" xml:space="preserve">
<value>Upstream branch не настроен и удаленный репозиторий 'origin' не найден.
Установите отслеживание вручную: git push -u origin {0}</value>
</data>
<data name="Git_ForcePushSuccess" xml:space="preserve">
<value>Force push выполнен успешно</value>
</data>
<data name="Git_ForcePushedTo" xml:space="preserve">
<value>Force push выполнен в origin/{0}</value>
</data>
<data name="Git_ProcessFailed" xml:space="preserve">
<value>Не удалось запустить процесс git</value>
</data>
<data name="Git_ForcePushSuccessCmd" xml:space="preserve">
<value>Force push выполнен успешно (через команду git)</value>
</data>
<data name="Git_PushFailed" xml:space="preserve">
<value>Push не выполнен: {0}</value>
</data>
<data name="Git_CommandFailed" xml:space="preserve">
<value>Не удалось выполнить команду git: {0}</value>
</data>
<data name="Git_NoUpstream" xml:space="preserve">
<value>Upstream branch не настроен. Установите отслеживание: git push -u origin &lt;branch&gt;</value>
</data>
<data name="Git_NonFastForward" xml:space="preserve">
<value>Push отклонен: non-fast-forward. Сначала выполните pull изменений или используйте force push.</value>
</data>
<data name="Git_PushSuccessCmd" xml:space="preserve">
<value>Push выполнен успешно (через команду git)</value>
</data>
<!-- ==================== Cleanup Executor ==================== -->
<data name="Cleanup_PushedCommitsBlocked" xml:space="preserve">
<value>Некоторые commit были отправлены. Включите 'AllowPushedCommits' для продолжения.</value>
</data>
<data name="Cleanup_NotImplemented" xml:space="preserve">
<value>Тип очистки '{0}' еще не реализован</value>
</data>
<data name="Cleanup_Rebuilding" xml:space="preserve">
<value>Перестроение истории commit...</value>
</data>
<data name="Cleanup_RebuildingCount" xml:space="preserve">
<value>Перестроение {0} commit...</value>
</data>
<data name="Cleanup_ProcessingCommit" xml:space="preserve">
<value>Обработка commit {0}/{1}...</value>
</data>
<data name="Cleanup_UpdatingBranch" xml:space="preserve">
<value>Обновление ссылки на branch...</value>
</data>
<data name="Cleanup_SquashingMerges" xml:space="preserve">
<value>Объединение merge commit...</value>
</data>
<data name="Cleanup_DropDuplicatesFailed" xml:space="preserve">
<value>Не удалось удалить дублирующиеся commit: {0}</value>
</data>
<data name="Cleanup_NeedTwoCommits" xml:space="preserve">
<value>Для объединения требуется как минимум 2 commit</value>
</data>
<data name="Cleanup_NoCommitsOnBranch" xml:space="preserve">
<value>В текущей branch не найдено commit</value>
</data>
<data name="Cleanup_NoMatchingCommits" xml:space="preserve">
<value>Не найдено подходящих commit для удаления</value>
</data>
<data name="Cleanup_NoCommitsToSquash" xml:space="preserve">
<value>Не указаны commit для объединения</value>
</data>
<data name="Cleanup_NoMergeCommits" xml:space="preserve">
<value>Не найдено подходящих merge commit для объединения</value>
</data>
<data name="Cleanup_SquashMergeFailed" xml:space="preserve">
<value>Не удалось объединить merge commit: {0}</value>
</data>
<data name="Cleanup_NoCommitsToFix" xml:space="preserve">
<value>Не указаны commit для исправления</value>
</data>
<data name="Cleanup_FixAuthorFailed" xml:space="preserve">
<value>Не удалось исправить авторство: {0}</value>
</data>
<data name="Cleanup_ConsolidatingFixes" xml:space="preserve">
<value>Консолидация commit с исправлениями merge...</value>
</data>
<data name="Cleanup_NoFixCommits" xml:space="preserve">
<value>Нет commit с исправлениями для консолидации</value>
</data>
<data name="Cleanup_NoMatchingFixes" xml:space="preserve">
<value>Не найдено подходящих commit с исправлениями для консолидации</value>
</data>
<data name="Cleanup_ConsolidateFailed" xml:space="preserve">
<value>Не удалось консолидировать commit с исправлениями merge: {0}</value>
</data>
<data name="Cleanup_ArchivingBranches" xml:space="preserve">
<value>Архивирование устаревших branch...</value>
</data>
<data name="Cleanup_ProcessingBranch" xml:space="preserve">
<value>Обработка branch {0}...</value>
</data>
<data name="Cleanup_ArchiveComplete" xml:space="preserve">
<value>Архивирование завершено</value>
</data>
<data name="Cleanup_ArchiveFailed" xml:space="preserve">
<value>Не удалось заархивировать branch: {0}</value>
</data>
<data name="Cleanup_AnalyzingStructure" xml:space="preserve">
<value>Анализ структуры branch...</value>
</data>
<data name="Cleanup_FoundCommits" xml:space="preserve">
<value>Найдено {0} commit для линеаризации...</value>
</data>
<data name="Cleanup_Linearizing" xml:space="preserve">
<value>Линеаризация {0} commit (удаление {1} merge)...</value>
</data>
<data name="Cleanup_RebuildingCommit" xml:space="preserve">
<value>Перестроение commit {0}/{1}...</value>
</data>
<data name="Cleanup_Reconciling" xml:space="preserve">
<value>Согласование окончательного состояния...</value>
</data>
<data name="Cleanup_AlreadyLinear" xml:space="preserve">
<value>История уже линейна - merge commit не найдены</value>
</data>
<data name="Cleanup_LinearizeComplete" xml:space="preserve">
<value>Линеаризация завершена</value>
</data>
<data name="Cleanup_LinearizeFailed" xml:space="preserve">
<value>Не удалось линеаризовать историю: {0}</value>
</data>
<data name="Cleanup_DescReword" xml:space="preserve">
<value>Будет переписано {0} сообщений commit для улучшения качества.</value>
</data>
<data name="Cleanup_DescSquash" xml:space="preserve">
<value>Будет объединено {0} дублирующихся commit в 1.</value>
</data>
<data name="Cleanup_DescConsolidate" xml:space="preserve">
<value>Будет консолидировано {0} commit с исправлениями merge.</value>
</data>
<data name="Cleanup_DescAuthorship" xml:space="preserve">
<value>Будет исправлено авторство в {0} commit.</value>
</data>
<data name="Cleanup_DescTrivialMerges" xml:space="preserve">
<value>Будет консолидировано {0} тривиальных merge.</value>
</data>
<data name="Cleanup_DescArchive" xml:space="preserve">
<value>Будут заархивированы устаревшие branch (удалены при слиянии, иначе помечены тегом).</value>
</data>
<data name="Cleanup_DescLinearize" xml:space="preserve">
<value>Будет линеаризована история путем удаления merge commit и сортировки по дате.</value>
</data>
<data name="Cleanup_DescGeneric" xml:space="preserve">
<value>Будет обработано {0} commit.</value>
</data>
<data name="Cleanup_ReconcileMerge" xml:space="preserve">
<value>Согласование: merge окончательного состояния после линеаризации</value>
</data>
<!-- ==================== Validation ==================== -->
<data name="Validation_WorkspaceRequired" xml:space="preserve">
<value>WorkspaceRoot обязателен</value>
</data>
<data name="Validation_WorkspaceNotFound" xml:space="preserve">
<value>Каталог WorkspaceRoot не существует: {0}</value>
</data>
<data name="Validation_MaxCommitsPositive" xml:space="preserve">
<value>MaxCommitsPerRepo должен быть больше 0</value>
</data>
<data name="Validation_RulesNull" xml:space="preserve">
<value>Rules не может быть null</value>
</data>
<data name="Validation_AiOptionsNull" xml:space="preserve">
<value>Параметры Ai не могут быть null</value>
</data>
<data name="Validation_InvalidOptions" xml:space="preserve">
<value>Недопустимые GitImproverOptions: {0}</value>
</data>
<data name="Validation_WeightsSum" xml:space="preserve">
<value>Сумма весов должна быть равна 1.0 (текущая: {0})</value>
</data>
<!-- ==================== Service Messages ==================== -->
<data name="Service_UnknownError" xml:space="preserve">
<value>Неизвестная ошибка</value>
</data>
<data name="Service_RepoNotRegistered" xml:space="preserve">
<value>Репозиторий не зарегистрирован</value>
</data>
<data name="Service_UncommittedChanges" xml:space="preserve">
<value>Невозможно перезаписать commit с незафиксированными изменениями. Пожалуйста, сначала выполните commit или stash изменений.</value>
</data>
<data name="Service_RepoNotFound" xml:space="preserve">
<value>Репозиторий не найден: {0}</value>
</data>
<data name="Service_NoSuggestion" xml:space="preserve">
<value>Предлагаемое сообщение недоступно</value>
</data>
<data name="Service_RepoNotRegisteredPath" xml:space="preserve">
<value>Репозиторий не зарегистрирован: {0}</value>
</data>
<data name="Service_ApiKeyNotConfigured" xml:space="preserve">
<value>API ключ не настроен. Пожалуйста, установите API ключ в настройках.</value>
</data>
<data name="Service_AiAnalysisFailed" xml:space="preserve">
<value>Не удалось выполнить анализ AI</value>
</data>
<data name="Service_AiFallback" xml:space="preserve">
<value>AI не вернул структурированный вывод - использовано исходное сообщение</value>
</data>
<data name="Service_PushSuccess" xml:space="preserve">
<value>Push выполнен успешно</value>
</data>
<!-- ==================== Health Analyzer Status ==================== -->
<data name="Health_LoadingCommits" xml:space="preserve">
<value>Загрузка commit</value>
</data>
<data name="Health_DetectingDuplicates" xml:space="preserve">
<value>Обнаружение дубликатов</value>
</data>
<data name="Health_AnalyzingMerges" xml:space="preserve">
<value>Анализ merge commit</value>
</data>
<data name="Health_AnalyzingBranches" xml:space="preserve">
<value>Анализ сложности branch</value>
</data>
<data name="Health_AnalyzingMessages" xml:space="preserve">
<value>Анализ качества сообщений</value>
</data>
<data name="Health_AnalyzingAuthorship" xml:space="preserve">
<value>Анализ авторства</value>
</data>
<data name="Health_Complete" xml:space="preserve">
<value>Завершено</value>
</data>
<!-- ==================== Health Report Issues ==================== -->
<data name="Report_DuplicateContent" xml:space="preserve">
<value>Дублирующиеся commit с идентичным содержимым</value>
</data>
<data name="Report_DuplicateContentDesc" xml:space="preserve">
<value>Найдено {0} групп commit с идентичным содержимым файлов ({1} избыточных commit). Их можно безопасно объединить, так как они имеют одинаковый SHA дерева.</value>
</data>
<data name="Report_DuplicateMessages" xml:space="preserve">
<value>Commit с дублирующимися сообщениями</value>
</data>
<data name="Report_DuplicateMessagesDesc" xml:space="preserve">
<value>Найдено {0} групп commit с идентичными сообщениями, но разными изменениями кода ({1} commit). Рассмотрите возможность использования более описательных сообщений для различения изменений.</value>
</data>
<data name="Report_ExcessiveMerges" xml:space="preserve">
<value>Избыточное количество merge commit</value>
</data>
<data name="Report_HighMergeRatio" xml:space="preserve">
<value>Высокое соотношение merge commit</value>
</data>
<data name="Report_MergeRatioDesc" xml:space="preserve">
<value>Ваш репозиторий имеет {0}% соотношение merge commit ({1}/{2} commit). Рассмотрите использование rebase workflow или squash merge.</value>
</data>
<data name="Report_MergeFixCommits" xml:space="preserve">
<value>Обнаружены commit с исправлениями merge</value>
</data>
<data name="Report_MergeFixDesc" xml:space="preserve">
<value>Найдено {0} commit с сообщениями типа 'fix merge', обнаруженных после merge.</value>
</data>
<data name="Report_CrossMerges" xml:space="preserve">
<value>Перекрестные merge между branch</value>
</data>
<data name="Report_CrossMergesDesc" xml:space="preserve">
<value>Обнаружено {0} перекрестных merge между feature branch. Используйте feature branch, которые сливаются только в main.</value>
</data>
<data name="Report_StaleBranches" xml:space="preserve">
<value>Устаревшие branch</value>
</data>
<data name="Report_StaleBranchesDesc" xml:space="preserve">
<value>Найдено {0} branch без активности более 30 дней.</value>
</data>
<!-- ==================== Safety Warnings ==================== -->
<data name="Safety_UncommittedChanges" xml:space="preserve">
<value>У вас есть незафиксированные изменения. Пожалуйста, сначала выполните commit или stash их.</value>
</data>
<data name="Safety_PushedCommits" xml:space="preserve">
<value>{0} commit уже были отправлены на удаленный сервер. Их перезапись потребует force push и может повлиять на других участников.</value>
</data>
<data name="Safety_BehindRemote" xml:space="preserve">
<value>Ваша branch отстает от удаленной на {0} commit. Рассмотрите возможность сначала выполнить pull, чтобы избежать конфликтов.</value>
</data>
<!-- ==================== Health Status ==================== -->
<data name="HealthStatus_NeedsAttention" xml:space="preserve">
<value>В репозитории есть заметные проблемы, которые следует устранить.</value>
</data>
<data name="HealthStatus_Critical" xml:space="preserve">
<value>Репозиторий требует немедленного внимания. История сильно ухудшена.</value>
</data>
</root>

View File

@@ -0,0 +1,421 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="0" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<!-- ==================== Commit Message Analyzer ==================== -->
<data name="Analyzer_MessageEmpty" xml:space="preserve">
<value>Commit 消息为空</value>
</data>
<data name="Analyzer_SubjectTooShort" xml:space="preserve">
<value>主题长度为 {0} 个字符,最小长度为 {1}</value>
</data>
<data name="Analyzer_SubjectTooLong" xml:space="preserve">
<value>主题长度为 {0} 个字符,建议最大长度为 {1}</value>
</data>
<data name="Analyzer_BannedPhrase" xml:space="preserve">
<value>主题使用了无意义的短语:'{0}'</value>
</data>
<data name="Analyzer_NotConventional" xml:space="preserve">
<value>消息未遵循约定式提交格式(类型: 主题)</value>
</data>
<data name="Analyzer_UnknownType" xml:space="preserve">
<value>未知的约定式提交类型:{0}</value>
</data>
<data name="Analyzer_NoIssueRef" xml:space="preserve">
<value>未找到问题引用(例如 #123 或 JIRA-123</value>
</data>
<data name="Analyzer_CapitalLetter" xml:space="preserve">
<value>主题应以大写字母开头</value>
</data>
<data name="Analyzer_NoPeriod" xml:space="preserve">
<value>主题不应以句号结尾</value>
</data>
<data name="Analyzer_ImperativeMood" xml:space="preserve">
<value>使用祈使语气:'{0}' → '{1}'(例如,使用 'Add' 而不是 'Added'</value>
</data>
<data name="Analyzer_BodyTooShort" xml:space="preserve">
<value>正文长度为 {0} 个字符,最小长度为 {1}</value>
</data>
<data name="Analyzer_BlankLine" xml:space="preserve">
<value>在主题和正文之间添加一个空行</value>
</data>
<data name="Analyzer_NotDescriptive" xml:space="preserve">
<value>'{0}' 未描述 {1} 个文件中的更改内容</value>
</data>
<data name="Analyzer_TooVague" xml:space="preserve">
<value>消息对于 {0} 个已更改的文件来说过于模糊 - 请描述具体更改了什么</value>
</data>
<data name="Analyzer_LargeChange" xml:space="preserve">
<value>较大的更改({0} 个文件,{1} 行)需要更详细的消息</value>
</data>
<data name="Analyzer_MajorChange" xml:space="preserve">
<value>重大更改({0} 个文件)应包含解释原因的正文</value>
</data>
<data name="Analyzer_MentionArea" xml:space="preserve">
<value>考虑提及更改的区域(文件:{0}</value>
</data>
<!-- ==================== Git Operations Service ==================== -->
<data name="Git_NoCommits" xml:space="preserve">
<value>仓库中没有 commit</value>
</data>
<data name="Git_CommitNotFound" xml:space="preserve">
<value>未找到 commit{0}</value>
</data>
<data name="Git_NotAncestor" xml:space="preserve">
<value>目标 commit 不是 HEAD 的祖先</value>
</data>
<data name="Git_NoTargetCommits" xml:space="preserve">
<value>仓库中未找到任何目标 commit</value>
</data>
<data name="Git_ParentMismatch" xml:space="preserve">
<value>Commit 创建失败commit {0} 的父提交不匹配</value>
</data>
<data name="Git_HeadUpdateFailed" xml:space="preserve">
<value>无法将 HEAD 更新到新的 commit {0}</value>
</data>
<data name="Git_VerificationFailed" xml:space="preserve">
<value>磁盘验证失败HEAD 应为 {0} 但实际为 {1}</value>
</data>
<data name="Git_OldCommitReachable" xml:space="preserve">
<value>重写后旧的 commit {0} 仍可从 HEAD 访问</value>
</data>
<data name="Git_Error" xml:space="preserve">
<value>Git 错误:{0}</value>
</data>
<data name="Git_RemoteNotFound" xml:space="preserve">
<value>未找到远程仓库 '{0}'</value>
</data>
<data name="Git_NoUpstreamNoOrigin" xml:space="preserve">
<value>未配置上游 branch 且未找到 'origin' 远程仓库。
请手动设置跟踪git push -u origin {0}</value>
</data>
<data name="Git_ForcePushSuccess" xml:space="preserve">
<value>强制 push 成功</value>
</data>
<data name="Git_ForcePushedTo" xml:space="preserve">
<value>已强制 push 到 origin/{0}</value>
</data>
<data name="Git_ProcessFailed" xml:space="preserve">
<value>无法启动 git 进程</value>
</data>
<data name="Git_ForcePushSuccessCmd" xml:space="preserve">
<value>强制 push 成功(通过 git 命令)</value>
</data>
<data name="Git_PushFailed" xml:space="preserve">
<value>Push 失败:{0}</value>
</data>
<data name="Git_CommandFailed" xml:space="preserve">
<value>无法运行 git 命令:{0}</value>
</data>
<data name="Git_NoUpstream" xml:space="preserve">
<value>未配置上游 branch。请设置跟踪git push -u origin &lt;branch&gt;</value>
</data>
<data name="Git_NonFastForward" xml:space="preserve">
<value>Push 被拒绝:非快进式。请先 pull 更改或使用强制 push。</value>
</data>
<data name="Git_PushSuccessCmd" xml:space="preserve">
<value>Push 成功(通过 git 命令)</value>
</data>
<!-- ==================== Cleanup Executor ==================== -->
<data name="Cleanup_PushedCommitsBlocked" xml:space="preserve">
<value>部分 commit 已被 push。启用 'AllowPushedCommits' 以继续。</value>
</data>
<data name="Cleanup_NotImplemented" xml:space="preserve">
<value>清理类型 '{0}' 尚未实现</value>
</data>
<data name="Cleanup_Rebuilding" xml:space="preserve">
<value>正在重建 commit 历史...</value>
</data>
<data name="Cleanup_RebuildingCount" xml:space="preserve">
<value>正在重建 {0} 个 commit...</value>
</data>
<data name="Cleanup_ProcessingCommit" xml:space="preserve">
<value>正在处理 commit {0}/{1}...</value>
</data>
<data name="Cleanup_UpdatingBranch" xml:space="preserve">
<value>正在更新 branch 引用...</value>
</data>
<data name="Cleanup_SquashingMerges" xml:space="preserve">
<value>正在压缩 merge commit...</value>
</data>
<data name="Cleanup_DropDuplicatesFailed" xml:space="preserve">
<value>删除重复 commit 失败:{0}</value>
</data>
<data name="Cleanup_NeedTwoCommits" xml:space="preserve">
<value>至少需要 2 个 commit 才能压缩</value>
</data>
<data name="Cleanup_NoCommitsOnBranch" xml:space="preserve">
<value>当前 branch 上未找到 commit</value>
</data>
<data name="Cleanup_NoMatchingCommits" xml:space="preserve">
<value>未找到要删除的匹配 commit</value>
</data>
<data name="Cleanup_NoCommitsToSquash" xml:space="preserve">
<value>未指定要压缩的 commit</value>
</data>
<data name="Cleanup_NoMergeCommits" xml:space="preserve">
<value>未找到要压缩的匹配 merge commit</value>
</data>
<data name="Cleanup_SquashMergeFailed" xml:space="preserve">
<value>压缩 merge commit 失败:{0}</value>
</data>
<data name="Cleanup_NoCommitsToFix" xml:space="preserve">
<value>未指定要修复的 commit</value>
</data>
<data name="Cleanup_FixAuthorFailed" xml:space="preserve">
<value>修复作者信息失败:{0}</value>
</data>
<data name="Cleanup_ConsolidatingFixes" xml:space="preserve">
<value>正在合并 merge 修复 commit...</value>
</data>
<data name="Cleanup_NoFixCommits" xml:space="preserve">
<value>没有要合并的修复 commit</value>
</data>
<data name="Cleanup_NoMatchingFixes" xml:space="preserve">
<value>未找到要合并的匹配修复 commit</value>
</data>
<data name="Cleanup_ConsolidateFailed" xml:space="preserve">
<value>合并 merge 修复 commit 失败:{0}</value>
</data>
<data name="Cleanup_ArchivingBranches" xml:space="preserve">
<value>正在归档陈旧的 branch...</value>
</data>
<data name="Cleanup_ProcessingBranch" xml:space="preserve">
<value>正在处理 branch {0}...</value>
</data>
<data name="Cleanup_ArchiveComplete" xml:space="preserve">
<value>归档完成</value>
</data>
<data name="Cleanup_ArchiveFailed" xml:space="preserve">
<value>归档 branch 失败:{0}</value>
</data>
<data name="Cleanup_AnalyzingStructure" xml:space="preserve">
<value>正在分析 branch 结构...</value>
</data>
<data name="Cleanup_FoundCommits" xml:space="preserve">
<value>找到 {0} 个要线性化的 commit...</value>
</data>
<data name="Cleanup_Linearizing" xml:space="preserve">
<value>正在线性化 {0} 个 commit移除 {1} 个 merge...</value>
</data>
<data name="Cleanup_RebuildingCommit" xml:space="preserve">
<value>正在重建 commit {0}/{1}...</value>
</data>
<data name="Cleanup_Reconciling" xml:space="preserve">
<value>正在协调最终状态...</value>
</data>
<data name="Cleanup_AlreadyLinear" xml:space="preserve">
<value>历史已是线性的 - 未找到 merge commit</value>
</data>
<data name="Cleanup_LinearizeComplete" xml:space="preserve">
<value>线性化完成</value>
</data>
<data name="Cleanup_LinearizeFailed" xml:space="preserve">
<value>线性化历史失败:{0}</value>
</data>
<data name="Cleanup_DescReword" xml:space="preserve">
<value>将重写 {0} 条 commit 消息以提高质量。</value>
</data>
<data name="Cleanup_DescSquash" xml:space="preserve">
<value>将把 {0} 个重复的 commit 压缩为 1 个。</value>
</data>
<data name="Cleanup_DescConsolidate" xml:space="preserve">
<value>将合并 {0} 个 merge 修复 commit。</value>
</data>
<data name="Cleanup_DescAuthorship" xml:space="preserve">
<value>将修复 {0} 个 commit 的作者信息。</value>
</data>
<data name="Cleanup_DescTrivialMerges" xml:space="preserve">
<value>将合并 {0} 个简单的 merge。</value>
</data>
<data name="Cleanup_DescArchive" xml:space="preserve">
<value>将归档陈旧的 branch如已合并则删除否则添加标签。</value>
</data>
<data name="Cleanup_DescLinearize" xml:space="preserve">
<value>将通过移除 merge commit 并按日期排序来线性化历史。</value>
</data>
<data name="Cleanup_DescGeneric" xml:space="preserve">
<value>将处理 {0} 个 commit。</value>
</data>
<data name="Cleanup_ReconcileMerge" xml:space="preserve">
<value>协调:在线性化后合并最终状态</value>
</data>
<!-- ==================== Validation ==================== -->
<data name="Validation_WorkspaceRequired" xml:space="preserve">
<value>需要 WorkspaceRoot</value>
</data>
<data name="Validation_WorkspaceNotFound" xml:space="preserve">
<value>WorkspaceRoot 目录不存在:{0}</value>
</data>
<data name="Validation_MaxCommitsPositive" xml:space="preserve">
<value>MaxCommitsPerRepo 必须大于 0</value>
</data>
<data name="Validation_RulesNull" xml:space="preserve">
<value>Rules 不能为 null</value>
</data>
<data name="Validation_AiOptionsNull" xml:space="preserve">
<value>Ai 选项不能为 null</value>
</data>
<data name="Validation_InvalidOptions" xml:space="preserve">
<value>无效的 GitImproverOptions{0}</value>
</data>
<data name="Validation_WeightsSum" xml:space="preserve">
<value>权重总和必须为 1.0(当前:{0}</value>
</data>
<!-- ==================== Service Messages ==================== -->
<data name="Service_UnknownError" xml:space="preserve">
<value>未知错误</value>
</data>
<data name="Service_RepoNotRegistered" xml:space="preserve">
<value>仓库未注册</value>
</data>
<data name="Service_UncommittedChanges" xml:space="preserve">
<value>存在未提交的更改,无法重写 commit。请先提交或暂存您的更改。</value>
</data>
<data name="Service_RepoNotFound" xml:space="preserve">
<value>未找到仓库:{0}</value>
</data>
<data name="Service_NoSuggestion" xml:space="preserve">
<value>没有可用的建议消息</value>
</data>
<data name="Service_RepoNotRegisteredPath" xml:space="preserve">
<value>仓库未注册:{0}</value>
</data>
<data name="Service_ApiKeyNotConfigured" xml:space="preserve">
<value>未配置 API 密钥。请在设置中设置您的 API 密钥。</value>
</data>
<data name="Service_AiAnalysisFailed" xml:space="preserve">
<value>AI 分析失败</value>
</data>
<data name="Service_AiFallback" xml:space="preserve">
<value>AI 未返回结构化输出 - 已回退到原始消息</value>
</data>
<data name="Service_PushSuccess" xml:space="preserve">
<value>Push 成功</value>
</data>
<!-- ==================== Health Analyzer Status ==================== -->
<data name="Health_LoadingCommits" xml:space="preserve">
<value>正在加载 commit</value>
</data>
<data name="Health_DetectingDuplicates" xml:space="preserve">
<value>正在检测重复项</value>
</data>
<data name="Health_AnalyzingMerges" xml:space="preserve">
<value>正在分析 merge commit</value>
</data>
<data name="Health_AnalyzingBranches" xml:space="preserve">
<value>正在分析 branch 复杂度</value>
</data>
<data name="Health_AnalyzingMessages" xml:space="preserve">
<value>正在分析消息质量</value>
</data>
<data name="Health_AnalyzingAuthorship" xml:space="preserve">
<value>正在分析作者信息</value>
</data>
<data name="Health_Complete" xml:space="preserve">
<value>完成</value>
</data>
<!-- ==================== Health Report Issues ==================== -->
<data name="Report_DuplicateContent" xml:space="preserve">
<value>内容相同的重复 commit</value>
</data>
<data name="Report_DuplicateContentDesc" xml:space="preserve">
<value>找到 {0} 组内容相同的 commit{1} 个冗余 commit。这些可以安全压缩因为它们具有相同的树 SHA。</value>
</data>
<data name="Report_DuplicateMessages" xml:space="preserve">
<value>消息重复的 commit</value>
</data>
<data name="Report_DuplicateMessagesDesc" xml:space="preserve">
<value>找到 {0} 组消息相同但代码更改不同的 commit{1} 个 commit。考虑使用更具描述性的消息来区分更改。</value>
</data>
<data name="Report_ExcessiveMerges" xml:space="preserve">
<value>过多的 merge commit</value>
</data>
<data name="Report_HighMergeRatio" xml:space="preserve">
<value>Merge commit 比例过高</value>
</data>
<data name="Report_MergeRatioDesc" xml:space="preserve">
<value>您的仓库有 {0}% 的 merge commit 比例({1}/{2} 个 commit。考虑使用 rebase 工作流或压缩 merge。</value>
</data>
<data name="Report_MergeFixCommits" xml:space="preserve">
<value>检测到 merge 修复 commit</value>
</data>
<data name="Report_MergeFixDesc" xml:space="preserve">
<value>在 merge 后找到 {0} 个类似 'fix merge' 的消息的 commit。</value>
</data>
<data name="Report_CrossMerges" xml:space="preserve">
<value>Branch 之间的交叉 merge</value>
</data>
<data name="Report_CrossMergesDesc" xml:space="preserve">
<value>检测到 {0} 个功能 branch 之间的交叉 merge。使用仅 merge 到主分支的功能 branch。</value>
</data>
<data name="Report_StaleBranches" xml:space="preserve">
<value>陈旧的 branch</value>
</data>
<data name="Report_StaleBranchesDesc" xml:space="preserve">
<value>找到 {0} 个超过 30 天没有活动的 branch。</value>
</data>
<!-- ==================== Safety Warnings ==================== -->
<data name="Safety_UncommittedChanges" xml:space="preserve">
<value>您有未提交的更改。请先提交或暂存它们。</value>
</data>
<data name="Safety_PushedCommits" xml:space="preserve">
<value>{0} 个 commit 已被 push 到远程仓库。重写它们将需要强制 push并可能影响协作者。</value>
</data>
<data name="Safety_BehindRemote" xml:space="preserve">
<value>您的 branch 落后远程仓库 {0} 个 commit。考虑先 pull 以避免冲突。</value>
</data>
<!-- ==================== Health Status ==================== -->
<data name="HealthStatus_NeedsAttention" xml:space="preserve">
<value>仓库存在应该解决的明显问题。</value>
</data>
<data name="HealthStatus_Critical" xml:space="preserve">
<value>仓库需要立即关注。历史严重退化。</value>
</data>
</root>

View File

@@ -0,0 +1,192 @@
using System.Globalization;
using System.Resources;
namespace MarketAlly.GitCommitEditor.Resources;
/// <summary>
/// Provides localized strings for the GitCommitEditor library.
/// </summary>
public static class Str
{
private static readonly ResourceManager ResourceManager =
new("MarketAlly.GitCommitEditor.Resources.Strings.LibStrings", typeof(Str).Assembly);
private static CultureInfo? _culture;
/// <summary>
/// Gets or sets the culture used for resource lookups.
/// If null, uses the current UI culture.
/// </summary>
public static CultureInfo? Culture
{
get => _culture;
set => _culture = value;
}
/// <summary>
/// Gets a localized string by key.
/// </summary>
public static string Get(string key)
{
return ResourceManager.GetString(key, _culture) ?? key;
}
/// <summary>
/// Gets a localized string by key with format arguments.
/// </summary>
public static string Get(string key, params object[] args)
{
var format = ResourceManager.GetString(key, _culture) ?? key;
return string.Format(format, args);
}
// ==================== Commit Message Analyzer ====================
public static string Analyzer_MessageEmpty => Get("Analyzer_MessageEmpty");
public static string Analyzer_SubjectTooShort(int length, int min) => Get("Analyzer_SubjectTooShort", length, min);
public static string Analyzer_SubjectTooLong(int length, int max) => Get("Analyzer_SubjectTooLong", length, max);
public static string Analyzer_BannedPhrase(string phrase) => Get("Analyzer_BannedPhrase", phrase);
public static string Analyzer_NotConventional => Get("Analyzer_NotConventional");
public static string Analyzer_UnknownType(string type) => Get("Analyzer_UnknownType", type);
public static string Analyzer_NoIssueRef => Get("Analyzer_NoIssueRef");
public static string Analyzer_CapitalLetter => Get("Analyzer_CapitalLetter");
public static string Analyzer_NoPeriod => Get("Analyzer_NoPeriod");
public static string Analyzer_ImperativeMood(string word, string suggestion) => Get("Analyzer_ImperativeMood", word, suggestion);
public static string Analyzer_BodyTooShort(int length, int min) => Get("Analyzer_BodyTooShort", length, min);
public static string Analyzer_BlankLine => Get("Analyzer_BlankLine");
public static string Analyzer_NotDescriptive(string subject, int fileCount) => Get("Analyzer_NotDescriptive", subject, fileCount);
public static string Analyzer_TooVague(int fileCount) => Get("Analyzer_TooVague", fileCount);
public static string Analyzer_LargeChange(int fileCount, int lineCount) => Get("Analyzer_LargeChange", fileCount, lineCount);
public static string Analyzer_MajorChange(int fileCount) => Get("Analyzer_MajorChange", fileCount);
public static string Analyzer_MentionArea(string extensions) => Get("Analyzer_MentionArea", extensions);
// ==================== Git Operations Service ====================
public static string Git_NoCommits => Get("Git_NoCommits");
public static string Git_CommitNotFound(string hash) => Get("Git_CommitNotFound", hash);
public static string Git_NotAncestor => Get("Git_NotAncestor");
public static string Git_NoTargetCommits => Get("Git_NoTargetCommits");
public static string Git_ParentMismatch(string sha) => Get("Git_ParentMismatch", sha);
public static string Git_HeadUpdateFailed(string sha) => Get("Git_HeadUpdateFailed", sha);
public static string Git_VerificationFailed(string expected, string actual) => Get("Git_VerificationFailed", expected, actual);
public static string Git_OldCommitReachable(string sha) => Get("Git_OldCommitReachable", sha);
public static string Git_Error(string message) => Get("Git_Error", message);
public static string Git_RemoteNotFound(string remote) => Get("Git_RemoteNotFound", remote);
public static string Git_NoUpstreamNoOrigin(string branch) => Get("Git_NoUpstreamNoOrigin", branch);
public static string Git_ForcePushSuccess => Get("Git_ForcePushSuccess");
public static string Git_ForcePushedTo(string branch) => Get("Git_ForcePushedTo", branch);
public static string Git_ProcessFailed => Get("Git_ProcessFailed");
public static string Git_ForcePushSuccessCmd => Get("Git_ForcePushSuccessCmd");
public static string Git_PushFailed(string message) => Get("Git_PushFailed", message);
public static string Git_CommandFailed(string message) => Get("Git_CommandFailed", message);
public static string Git_NoUpstream => Get("Git_NoUpstream");
public static string Git_NonFastForward => Get("Git_NonFastForward");
public static string Git_PushSuccessCmd => Get("Git_PushSuccessCmd");
// ==================== Cleanup Executor ====================
public static string Cleanup_PushedCommitsBlocked => Get("Cleanup_PushedCommitsBlocked");
public static string Cleanup_NotImplemented(string type) => Get("Cleanup_NotImplemented", type);
public static string Cleanup_Rebuilding => Get("Cleanup_Rebuilding");
public static string Cleanup_RebuildingCount(int count) => Get("Cleanup_RebuildingCount", count);
public static string Cleanup_ProcessingCommit(int current, int total) => Get("Cleanup_ProcessingCommit", current, total);
public static string Cleanup_UpdatingBranch => Get("Cleanup_UpdatingBranch");
public static string Cleanup_SquashingMerges => Get("Cleanup_SquashingMerges");
public static string Cleanup_DropDuplicatesFailed(string message) => Get("Cleanup_DropDuplicatesFailed", message);
public static string Cleanup_NeedTwoCommits => Get("Cleanup_NeedTwoCommits");
public static string Cleanup_NoCommitsOnBranch => Get("Cleanup_NoCommitsOnBranch");
public static string Cleanup_NoMatchingCommits => Get("Cleanup_NoMatchingCommits");
public static string Cleanup_NoCommitsToSquash => Get("Cleanup_NoCommitsToSquash");
public static string Cleanup_NoMergeCommits => Get("Cleanup_NoMergeCommits");
public static string Cleanup_SquashMergeFailed(string message) => Get("Cleanup_SquashMergeFailed", message);
public static string Cleanup_NoCommitsToFix => Get("Cleanup_NoCommitsToFix");
public static string Cleanup_FixAuthorFailed(string message) => Get("Cleanup_FixAuthorFailed", message);
public static string Cleanup_ConsolidatingFixes => Get("Cleanup_ConsolidatingFixes");
public static string Cleanup_NoFixCommits => Get("Cleanup_NoFixCommits");
public static string Cleanup_NoMatchingFixes => Get("Cleanup_NoMatchingFixes");
public static string Cleanup_ConsolidateFailed(string message) => Get("Cleanup_ConsolidateFailed", message);
public static string Cleanup_ArchivingBranches => Get("Cleanup_ArchivingBranches");
public static string Cleanup_ProcessingBranch(string branch) => Get("Cleanup_ProcessingBranch", branch);
public static string Cleanup_ArchiveComplete => Get("Cleanup_ArchiveComplete");
public static string Cleanup_ArchiveFailed(string message) => Get("Cleanup_ArchiveFailed", message);
public static string Cleanup_AnalyzingStructure => Get("Cleanup_AnalyzingStructure");
public static string Cleanup_FoundCommits(int count) => Get("Cleanup_FoundCommits", count);
public static string Cleanup_Linearizing(int commits, int merges) => Get("Cleanup_Linearizing", commits, merges);
public static string Cleanup_RebuildingCommit(int current, int total) => Get("Cleanup_RebuildingCommit", current, total);
public static string Cleanup_Reconciling => Get("Cleanup_Reconciling");
public static string Cleanup_AlreadyLinear => Get("Cleanup_AlreadyLinear");
public static string Cleanup_LinearizeComplete => Get("Cleanup_LinearizeComplete");
public static string Cleanup_LinearizeFailed(string message) => Get("Cleanup_LinearizeFailed", message);
public static string Cleanup_DescReword(int count) => Get("Cleanup_DescReword", count);
public static string Cleanup_DescSquash(int count) => Get("Cleanup_DescSquash", count);
public static string Cleanup_DescSquashMerges(int count) => Get("Cleanup_DescTrivialMerges", count);
public static string Cleanup_DescConsolidate(int count) => Get("Cleanup_DescConsolidate", count);
public static string Cleanup_DescAuthorship(int count) => Get("Cleanup_DescAuthorship", count);
public static string Cleanup_DescFixAuthor(int count) => Get("Cleanup_DescAuthorship", count);
public static string Cleanup_DescTrivialMerges(int count) => Get("Cleanup_DescTrivialMerges", count);
public static string Cleanup_DescArchive => Get("Cleanup_DescArchive");
public static string Cleanup_DescLinearize => Get("Cleanup_DescLinearize");
public static string Cleanup_DescGeneric(int count) => Get("Cleanup_DescGeneric", count);
public static string Cleanup_DescDefault(int count) => Get("Cleanup_DescGeneric", count);
public static string Cleanup_ReconcileMerge => Get("Cleanup_ReconcileMerge");
// ==================== Validation ====================
public static string Validation_WorkspaceRequired => Get("Validation_WorkspaceRequired");
public static string Validation_WorkspaceNotFound(string path) => Get("Validation_WorkspaceNotFound", path);
public static string Validation_MaxCommitsPositive => Get("Validation_MaxCommitsPositive");
public static string Validation_RulesNull => Get("Validation_RulesNull");
public static string Validation_AiOptionsNull => Get("Validation_AiOptionsNull");
public static string Validation_InvalidOptions(string messages) => Get("Validation_InvalidOptions", messages);
public static string Validation_WeightsSum(double sum) => Get("Validation_WeightsSum", sum);
// ==================== Service Messages ====================
public static string Service_UnknownError => Get("Service_UnknownError");
public static string Service_RepoNotRegistered => Get("Service_RepoNotRegistered");
public static string Service_UncommittedChanges => Get("Service_UncommittedChanges");
public static string Service_RepoNotFound(string id) => Get("Service_RepoNotFound", id);
public static string Service_NoSuggestion => Get("Service_NoSuggestion");
public static string Service_RepoNotRegisteredPath(string path) => Get("Service_RepoNotRegisteredPath", path);
public static string Service_ApiKeyNotConfigured => Get("Service_ApiKeyNotConfigured");
public static string Service_AiAnalysisFailed => Get("Service_AiAnalysisFailed");
public static string Service_AiFallback => Get("Service_AiFallback");
public static string Service_PushSuccess => Get("Service_PushSuccess");
// ==================== Health Analyzer Status ====================
public static string Health_LoadingCommits => Get("Health_LoadingCommits");
public static string Health_DetectingDuplicates => Get("Health_DetectingDuplicates");
public static string Health_AnalyzingMerges => Get("Health_AnalyzingMerges");
public static string Health_AnalyzingBranches => Get("Health_AnalyzingBranches");
public static string Health_AnalyzingMessages => Get("Health_AnalyzingMessages");
public static string Health_AnalyzingAuthorship => Get("Health_AnalyzingAuthorship");
public static string Health_Complete => Get("Health_Complete");
// ==================== Health Report Issues ====================
public static string Report_DuplicateContent => Get("Report_DuplicateContent");
public static string Report_DuplicateContentDesc(int groups, int redundant) => Get("Report_DuplicateContentDesc", groups, redundant);
public static string Report_DuplicateMessages => Get("Report_DuplicateMessages");
public static string Report_DuplicateMessagesDesc(int groups, int commits) => Get("Report_DuplicateMessagesDesc", groups, commits);
public static string Report_ExcessiveMerges => Get("Report_ExcessiveMerges");
public static string Report_HighMergeRatio => Get("Report_HighMergeRatio");
public static string Report_MergeRatioDesc(int ratio, int merges, int total) => Get("Report_MergeRatioDesc", ratio, merges, total);
public static string Report_MergeFixCommits => Get("Report_MergeFixCommits");
public static string Report_MergeFixDesc(int count) => Get("Report_MergeFixDesc", count);
public static string Report_CrossMerges => Get("Report_CrossMerges");
public static string Report_CrossMergesDesc(int count) => Get("Report_CrossMergesDesc", count);
public static string Report_StaleBranches => Get("Report_StaleBranches");
public static string Report_StaleBranchesDesc(int count) => Get("Report_StaleBranchesDesc", count);
// ==================== Safety Warnings ====================
public static string Safety_UncommittedChanges => Get("Safety_UncommittedChanges");
public static string Safety_PushedCommits(int count) => Get("Safety_PushedCommits", count);
public static string Safety_BehindRemote(int count) => Get("Safety_BehindRemote", count);
// ==================== Health Status ====================
public static string HealthStatus_NeedsAttention => Get("HealthStatus_NeedsAttention");
public static string HealthStatus_Critical => Get("HealthStatus_Critical");
}

199
Rewriters/AiCommitRewriter.cs Executable file
View File

@@ -0,0 +1,199 @@
using System.Text;
using MarketAlly.AIPlugin;
using MarketAlly.AIPlugin.Conversation;
using MarketAlly.GitCommitEditor.Models;
using MarketAlly.GitCommitEditor.Options;
using MarketAlly.GitCommitEditor.Plugins;
using MarketAlly.GitCommitEditor.Services;
namespace MarketAlly.GitCommitEditor.Rewriters;
/// <summary>
/// Commit message rewriter using MarketAlly.AIPlugin's AIConversation API.
/// Supports multiple AI providers (Claude, OpenAI, Gemini, Mistral, Qwen).
/// </summary>
public sealed class AICommitRewriter : ICommitMessageRewriter, IDisposable
{
private const string OperationType = "CommitSuggestion";
private readonly AiOptions _options;
private readonly AIConversation _conversation;
private readonly ICostTrackingService? _costTracker;
public AICommitRewriter(AiOptions options, ICostTrackingService? costTracker = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_costTracker = costTracker;
var provider = ParseProvider(options.Provider);
_conversation = AIConversationBuilder.Create()
.UseProvider(provider, options.ApiKey)
.UseModel(options.Model)
.WithSystemPrompt(BuildSystemPrompt())
.WithTemperature(0.3) // Lower temperature for more consistent output
.WithMaxTokens(options.MaxTokens)
.WithToolExecutionLimit(3)
.RegisterPlugin<ReturnCommitMessagePlugin>()
.Build();
}
private static AIProvider ParseProvider(string? providerName)
{
return providerName?.ToLowerInvariant() switch
{
"claude" or "anthropic" => AIProvider.Claude,
"openai" or "gpt" => AIProvider.OpenAI,
"gemini" or "google" => AIProvider.Gemini,
"mistral" => AIProvider.Mistral,
"qwen" or "alibaba" => AIProvider.Qwen,
_ => AIProvider.Claude // Default to Claude
};
}
private static string BuildSystemPrompt()
{
return """
You are a git commit message expert. Your task is to improve commit messages following conventional commit format.
RULES:
1. Description: max 60 chars, imperative mood ("add" not "added"), no period, lowercase start
2. Scope is optional - use it for the area of codebase (e.g., "api", "ui", "auth")
3. Be specific about WHAT changed based on the files and diff
4. Add a body ONLY if the change is complex and needs explanation
5. Body should explain WHY, not repeat WHAT
When you receive commit information, call ReturnCommitMessage with the improved message.
""";
}
public async Task<SuggestionResult> SuggestImprovedMessageAsync(CommitAnalysis analysis, CancellationToken ct = default)
{
var prompt = BuildPrompt(analysis);
var response = await _conversation.SendForStructuredOutputAsync<CommitMessageResult>(
prompt,
"ReturnCommitMessage",
ct);
// Extract cost information from response
var inputTokens = response.InputTokens;
var outputTokens = response.OutputTokens;
var cost = response.EstimatedCost ?? 0m;
// Record cost if tracking service is available
_costTracker?.RecordOperation(OperationType, inputTokens, outputTokens, cost);
if (response.StructuredOutput is CommitMessageResult result)
{
// Combine subject and body
var suggestion = string.IsNullOrWhiteSpace(result.Body)
? result.Subject.Trim()
: $"{result.Subject.Trim()}\n\n{result.Body.Trim()}";
// Check if the suggestion is actually different from original
if (string.Equals(suggestion, analysis.OriginalMessage, StringComparison.Ordinal))
{
return SuggestionResult.FailedWithOriginal(
analysis.OriginalMessage,
$"AI returned same message as original. Raw: {response.FinalMessage}",
inputTokens, outputTokens, cost);
}
return SuggestionResult.Succeeded(suggestion, inputTokens, outputTokens, cost);
}
// AI failed to return structured output
return SuggestionResult.FailedWithOriginal(
analysis.OriginalMessage,
$"No structured output. FinalMessage: {response.FinalMessage ?? "(null)"}",
inputTokens, outputTokens, cost);
}
public async Task<IReadOnlyList<(CommitAnalysis Analysis, SuggestionResult Result)>> SuggestBatchAsync(
IEnumerable<CommitAnalysis> analyses,
IProgress<int>? progress = null,
CancellationToken ct = default)
{
var results = new List<(CommitAnalysis, SuggestionResult)>();
var analysisList = analyses.ToList();
var processed = 0;
foreach (var analysis in analysisList)
{
if (ct.IsCancellationRequested) break;
try
{
var result = await SuggestImprovedMessageAsync(analysis, ct);
results.Add((analysis, result));
}
catch (Exception ex)
{
results.Add((analysis, SuggestionResult.Failed(ex.Message)));
}
processed++;
progress?.Report(processed);
// Rate limiting delay between requests
if (processed < analysisList.Count && _options.RateLimitDelayMs > 0)
{
await Task.Delay(_options.RateLimitDelayMs, ct);
}
}
return results;
}
private string BuildPrompt(CommitAnalysis analysis)
{
var sb = new StringBuilder();
sb.AppendLine("Please improve this commit message:");
sb.AppendLine();
sb.AppendLine($"ORIGINAL MESSAGE: {analysis.OriginalMessage}");
sb.AppendLine();
sb.AppendLine("FILES CHANGED:");
var files = analysis.FilesChanged.Take(20).ToList();
foreach (var file in files)
{
sb.AppendLine($" - {file}");
}
if (analysis.FilesChanged.Count > 20)
{
sb.AppendLine($" ... and {analysis.FilesChanged.Count - 20} more files");
}
sb.AppendLine();
sb.AppendLine($"STATS: +{analysis.LinesAdded} -{analysis.LinesDeleted} lines");
if (!string.IsNullOrEmpty(analysis.DiffSummary) && _options.IncludeDiffContext)
{
sb.AppendLine();
sb.AppendLine("DIFF SUMMARY:");
var diffLines = analysis.DiffSummary.Split('\n').Take(_options.MaxDiffLines);
sb.AppendLine(string.Join('\n', diffLines));
}
if (analysis.Quality.Issues.Count > 0)
{
sb.AppendLine();
sb.AppendLine("ISSUES WITH CURRENT MESSAGE:");
foreach (var issue in analysis.Quality.Issues)
{
sb.AppendLine($" - [{issue.Severity}] {issue.Message}");
}
}
sb.AppendLine();
sb.AppendLine("Call ReturnCommitMessage with the improved message.");
return sb.ToString();
}
public void Dispose()
{
// AIConversation handles its own disposal
}
}

View File

@@ -0,0 +1,87 @@
using MarketAlly.GitCommitEditor.Models;
using MarketAlly.GitCommitEditor.Options;
using MarketAlly.GitCommitEditor.Resources;
using MarketAlly.GitCommitEditor.Services;
namespace MarketAlly.GitCommitEditor.Rewriters;
/// <summary>
/// Exception thrown when AI features are used without an API key configured.
/// </summary>
public class ApiKeyNotConfiguredException : InvalidOperationException
{
public ApiKeyNotConfiguredException()
: base(Str.Service_ApiKeyNotConfigured)
{
}
}
/// <summary>
/// Dynamic commit rewriter that uses AI when configured, or throws if not.
/// </summary>
public sealed class DynamicCommitRewriter : ICommitMessageRewriter, IDisposable
{
private readonly AiOptions _options;
private readonly ICostTrackingService? _costTracker;
private AICommitRewriter? _aiRewriter;
private string? _lastApiKey;
private string? _lastProvider;
private string? _lastModel;
public DynamicCommitRewriter(AiOptions options, ICostTrackingService? costTracker = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_costTracker = costTracker;
}
/// <summary>
/// Returns true if an API key is configured and AI features are available.
/// </summary>
public bool IsConfigured => !string.IsNullOrEmpty(_options.ApiKey);
private AICommitRewriter GetRewriter()
{
// If no API key, throw exception
if (string.IsNullOrEmpty(_options.ApiKey))
{
throw new ApiKeyNotConfiguredException();
}
// Check if we need to create/recreate the AI rewriter
// (API key, provider, or model changed)
if (_aiRewriter == null ||
_lastApiKey != _options.ApiKey ||
_lastProvider != _options.Provider ||
_lastModel != _options.Model)
{
// Dispose old rewriter if exists
_aiRewriter?.Dispose();
// Create new rewriter with current settings
_aiRewriter = new AICommitRewriter(_options, _costTracker);
_lastApiKey = _options.ApiKey;
_lastProvider = _options.Provider;
_lastModel = _options.Model;
}
return _aiRewriter;
}
public Task<SuggestionResult> SuggestImprovedMessageAsync(CommitAnalysis analysis, CancellationToken ct = default)
{
return GetRewriter().SuggestImprovedMessageAsync(analysis, ct);
}
public Task<IReadOnlyList<(CommitAnalysis Analysis, SuggestionResult Result)>> SuggestBatchAsync(
IEnumerable<CommitAnalysis> analyses,
IProgress<int>? progress = null,
CancellationToken ct = default)
{
return GetRewriter().SuggestBatchAsync(analyses, progress, ct);
}
public void Dispose()
{
_aiRewriter?.Dispose();
}
}

View File

@@ -0,0 +1,13 @@
using MarketAlly.GitCommitEditor.Models;
namespace MarketAlly.GitCommitEditor.Rewriters;
public interface ICommitMessageRewriter
{
Task<SuggestionResult> SuggestImprovedMessageAsync(CommitAnalysis analysis, CancellationToken ct = default);
Task<IReadOnlyList<(CommitAnalysis Analysis, SuggestionResult Result)>> SuggestBatchAsync(
IEnumerable<CommitAnalysis> analyses,
IProgress<int>? progress = null,
CancellationToken ct = default);
}

1319
Services/CleanupExecutor.cs Executable file
View File

File diff suppressed because it is too large Load Diff

337
Services/CommitMessageAnalyzer.cs Executable file
View File

@@ -0,0 +1,337 @@
using System.Text.RegularExpressions;
using MarketAlly.GitCommitEditor.Models;
using MarketAlly.GitCommitEditor.Options;
using MarketAlly.GitCommitEditor.Resources;
namespace MarketAlly.GitCommitEditor.Services;
public sealed partial class CommitMessageAnalyzer : ICommitMessageAnalyzer
{
private readonly CommitMessageRules _rules;
// Vague/lazy words that don't describe what actually changed
private static readonly string[] VagueWords =
[
"updates", "update", "changes", "change", "stuff", "things",
"misc", "various", "some", "minor", "edits", "tweaks", "wip",
"work in progress", "modifications", "adjustments"
];
// Pattern: "ProjectName updates" or "ProjectName changes" - lazy naming
[GeneratedRegex(@"^\w+\s+(updates?|changes?|edits?|tweaks?|modifications?)$", RegexOptions.IgnoreCase)]
private static partial Regex LazyProjectNamePattern();
[GeneratedRegex(@"^(?<type>\w+)(?:\((?<scope>[^)]+)\))?!?:\s*(?<subject>.+)$", RegexOptions.Singleline)]
private static partial Regex ConventionalCommitPattern();
[GeneratedRegex(@"#\d+|[A-Z]{2,}-\d+", RegexOptions.IgnoreCase)]
private static partial Regex IssueReferencePattern();
[GeneratedRegex(@"\w+\([^)]+\):\s+.+")]
private static partial Regex ScopePattern();
public CommitMessageAnalyzer(CommitMessageRules rules)
{
_rules = rules ?? throw new ArgumentNullException(nameof(rules));
}
/// <summary>
/// Analyze without context - uses default context with 0 files.
/// </summary>
public MessageQualityScore Analyze(string message) => Analyze(message, new CommitContext());
/// <summary>
/// Analyze with full context about the changes for smarter detection.
/// </summary>
public MessageQualityScore Analyze(string message, CommitContext context)
{
if (string.IsNullOrWhiteSpace(message))
{
return new MessageQualityScore
{
OverallScore = 0,
Issues =
[
new QualityIssue
{
Severity = IssueSeverity.Error,
Code = "EMPTY_MESSAGE",
Message = Str.Analyzer_MessageEmpty
}
]
};
}
var issues = new List<QualityIssue>();
var lines = message.Split('\n', StringSplitOptions.None);
var subject = lines[0].Trim();
var body = lines.Length > 2 ? string.Join('\n', lines.Skip(2)).Trim() : string.Empty;
int score = 100;
// Subject length checks
if (subject.Length < _rules.MinSubjectLength)
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Error,
Code = "SUBJECT_TOO_SHORT",
Message = Str.Analyzer_SubjectTooShort(subject.Length, _rules.MinSubjectLength)
});
score -= 25;
}
if (subject.Length > _rules.MaxSubjectLength)
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Warning,
Code = "SUBJECT_TOO_LONG",
Message = Str.Analyzer_SubjectTooLong(subject.Length, _rules.MaxSubjectLength)
});
score -= 10;
}
// Banned phrases check
// For conventional commits (type: subject), check the subject part after the colon
// For non-conventional commits, check the whole subject
var lowerSubject = subject.ToLowerInvariant();
var conventionalMatch = ConventionalCommitPattern().Match(subject);
var textToCheck = conventionalMatch.Success
? conventionalMatch.Groups["subject"].Value.ToLowerInvariant().Trim()
: lowerSubject;
foreach (var banned in _rules.BannedPhrases)
{
var bannedLower = banned.ToLowerInvariant();
if (textToCheck == bannedLower ||
textToCheck.StartsWith(bannedLower + " ") ||
textToCheck.StartsWith(bannedLower + ":") ||
textToCheck.StartsWith(bannedLower + "."))
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Error,
Code = "BANNED_PHRASE",
Message = Str.Analyzer_BannedPhrase(banned)
});
score -= 30;
break;
}
}
// Conventional commit check (conventionalMatch already computed above)
if (_rules.RequireConventionalCommit)
{
if (!conventionalMatch.Success)
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Error,
Code = "NOT_CONVENTIONAL",
Message = Str.Analyzer_NotConventional
});
score -= 20;
}
else if (!_rules.ConventionalTypes.Contains(conventionalMatch.Groups["type"].Value.ToLower()))
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Warning,
Code = "UNKNOWN_TYPE",
Message = Str.Analyzer_UnknownType(conventionalMatch.Groups["type"].Value)
});
score -= 10;
}
}
// Issue reference check
if (_rules.RequireIssueReference)
{
if (!IssueReferencePattern().IsMatch(message))
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Warning,
Code = "NO_ISSUE_REF",
Message = Str.Analyzer_NoIssueRef
});
score -= 10;
}
}
// Capitalization (skip if conventional commit)
if (subject.Length > 0 && !conventionalMatch.Success)
{
if (char.IsLower(subject[0]))
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Info,
Code = "LOWERCASE_START",
Message = Str.Analyzer_CapitalLetter
});
score -= 5;
}
}
// Trailing period
if (subject.EndsWith('.'))
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Info,
Code = "TRAILING_PERIOD",
Message = Str.Analyzer_NoPeriod
});
score -= 5;
}
// Imperative mood check (simple heuristic)
var pastTenseIndicators = new[] { "added", "fixed", "updated", "changed", "removed", "deleted", "created" };
var firstWord = subject.Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault()?.ToLower() ?? "";
if (pastTenseIndicators.Contains(firstWord))
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Info,
Code = "NOT_IMPERATIVE",
Message = Str.Analyzer_ImperativeMood(firstWord, firstWord.TrimEnd('d', 'e'))
});
score -= 5;
}
// Body length check
if (_rules.MinBodyLength > 0 && body.Length < _rules.MinBodyLength)
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Warning,
Code = "BODY_TOO_SHORT",
Message = Str.Analyzer_BodyTooShort(body.Length, _rules.MinBodyLength)
});
score -= 10;
}
// Blank line between subject and body
if (lines.Length > 1 && !string.IsNullOrWhiteSpace(lines[1]))
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Info,
Code = "NO_BLANK_LINE",
Message = Str.Analyzer_BlankLine
});
score -= 5;
}
// === CONTEXT-AWARE CHECKS ===
// These checks consider the actual scope of changes
if (context.FilesChanged > 0)
{
var totalLinesChanged = context.LinesAdded + context.LinesDeleted;
// Check for lazy "ProjectName updates" pattern
if (LazyProjectNamePattern().IsMatch(subject))
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Error,
Code = "LAZY_MESSAGE",
Message = Str.Analyzer_NotDescriptive(subject, context.FilesChanged)
});
score -= 35;
}
// Check for vague words with significant changes
var subjectWords = subject.ToLowerInvariant().Split(' ', StringSplitOptions.RemoveEmptyEntries);
var vagueWordCount = subjectWords.Count(w => VagueWords.Contains(w));
var hasOnlyVagueContent = vagueWordCount > 0 && subjectWords.Length <= 3;
if (hasOnlyVagueContent && context.FilesChanged >= 3)
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Error,
Code = "VAGUE_FOR_SCOPE",
Message = Str.Analyzer_TooVague(context.FilesChanged)
});
score -= 30;
}
// Large changes need descriptive messages
if (context.FilesChanged >= 10 && subject.Length < 30)
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Warning,
Code = "LARGE_CHANGE_SHORT_MSG",
Message = Str.Analyzer_LargeChange(context.FilesChanged, totalLinesChanged)
});
score -= 15;
}
// Very large changes should have a body explaining context
if (context.FilesChanged >= 20 && body.Length < 20)
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Warning,
Code = "LARGE_CHANGE_NO_BODY",
Message = Str.Analyzer_MajorChange(context.FilesChanged)
});
score -= 10;
}
// Message doesn't mention any file types or areas changed
// Skip if message already has a scope pattern like feat(api): description
if (context.FilesChanged >= 5 && context.FileNames.Count > 0)
{
// Check: word(scope): description AND body has content
var hasScope = ScopePattern().IsMatch(subject);
var bodyWords = body.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
var hasBody = bodyWords >= 3;
// Only check for scope hint if no proper scope+body present
if (!(hasScope && hasBody))
{
var extensions = context.FileNames
.Select(f => Path.GetExtension(f).TrimStart('.').ToLower())
.Where(e => !string.IsNullOrEmpty(e))
.Distinct()
.ToList();
var folders = context.FileNames
.Select(f => Path.GetDirectoryName(f)?.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).LastOrDefault())
.Where(d => !string.IsNullOrEmpty(d))
.Distinct()
.Take(5)
.ToList();
// Check if message mentions any relevant context
var messageLower = message.ToLowerInvariant();
var mentionsFileType = extensions.Any(e => messageLower.Contains(e));
var mentionsFolder = folders.Any(f => messageLower.Contains(f!.ToLowerInvariant()));
if (!mentionsFileType && !mentionsFolder && extensions.Count > 1)
{
issues.Add(new QualityIssue
{
Severity = IssueSeverity.Info,
Code = "NO_SCOPE_HINT",
Message = Str.Analyzer_MentionArea(string.Join(", ", extensions.Take(3)))
});
score -= 5;
}
}
}
}
return new MessageQualityScore
{
OverallScore = Math.Max(0, score),
Issues = issues
};
}
}

View File

@@ -0,0 +1,191 @@
using System.Collections.Concurrent;
using System.Text.Json;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Persistence provider interface for cost tracking
/// </summary>
public interface ICostPersistenceProvider
{
string? GetValue(string key);
void SetValue(string key, string value);
void Remove(string key);
}
/// <summary>
/// Tracks AI operation costs for the session with persistence support
/// </summary>
public class CostTrackingService : ICostTrackingService
{
private const string KeyLifetimeCost = "cost_lifetime_total";
private const string KeyLifetimeOperations = "cost_lifetime_operations";
private const string KeyLifetimeInputTokens = "cost_lifetime_input_tokens";
private const string KeyLifetimeOutputTokens = "cost_lifetime_output_tokens";
private readonly ConcurrentDictionary<string, OperationCostSummary> _costBreakdown = new();
private readonly ICostPersistenceProvider? _persistence;
private readonly object _costLock = new();
private decimal _sessionCost;
private int _totalInputTokens;
private int _totalOutputTokens;
private int _operationCount;
private decimal _lifetimeCost;
private int _lifetimeInputTokens;
private int _lifetimeOutputTokens;
private int _lifetimeOperationCount;
public decimal SessionCost => _sessionCost;
public decimal LifetimeCost => _lifetimeCost;
public int TotalInputTokens => _totalInputTokens;
public int TotalOutputTokens => _totalOutputTokens;
public int OperationCount => _operationCount;
public int LifetimeOperationCount => _lifetimeOperationCount;
public event EventHandler<CostUpdatedEventArgs>? CostUpdated;
public CostTrackingService(ICostPersistenceProvider? persistence = null)
{
_persistence = persistence;
LoadState();
}
public void RecordOperation(string operationType, int inputTokens, int outputTokens, decimal cost)
{
// Update session totals
Interlocked.Add(ref _totalInputTokens, inputTokens);
Interlocked.Add(ref _totalOutputTokens, outputTokens);
Interlocked.Increment(ref _operationCount);
// Update lifetime totals
Interlocked.Add(ref _lifetimeInputTokens, inputTokens);
Interlocked.Add(ref _lifetimeOutputTokens, outputTokens);
Interlocked.Increment(ref _lifetimeOperationCount);
// Thread-safe decimal addition using lock
lock (_costLock)
{
_sessionCost += cost;
_lifetimeCost += cost;
}
// Update breakdown
_costBreakdown.AddOrUpdate(
operationType,
_ => new OperationCostSummary
{
OperationType = operationType,
Count = 1,
TotalInputTokens = inputTokens,
TotalOutputTokens = outputTokens,
TotalCost = cost
},
(_, existing) =>
{
existing.Count++;
existing.TotalInputTokens += inputTokens;
existing.TotalOutputTokens += outputTokens;
existing.TotalCost += cost;
return existing;
});
// Auto-save after each operation
SaveState();
// Raise event
CostUpdated?.Invoke(this, new CostUpdatedEventArgs(
operationType,
cost,
_sessionCost,
inputTokens,
outputTokens));
}
public IReadOnlyDictionary<string, OperationCostSummary> GetCostBreakdown()
{
return _costBreakdown.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
public void ResetSession()
{
_costBreakdown.Clear();
_sessionCost = 0;
_totalInputTokens = 0;
_totalOutputTokens = 0;
_operationCount = 0;
CostUpdated?.Invoke(this, new CostUpdatedEventArgs("SessionReset", 0, 0, 0, 0));
}
public void ResetAll()
{
ResetSession();
_lifetimeCost = 0;
_lifetimeInputTokens = 0;
_lifetimeOutputTokens = 0;
_lifetimeOperationCount = 0;
// Clear persisted data
_persistence?.Remove(KeyLifetimeCost);
_persistence?.Remove(KeyLifetimeOperations);
_persistence?.Remove(KeyLifetimeInputTokens);
_persistence?.Remove(KeyLifetimeOutputTokens);
CostUpdated?.Invoke(this, new CostUpdatedEventArgs("AllReset", 0, 0, 0, 0));
}
public void SaveState()
{
if (_persistence == null) return;
try
{
_persistence.SetValue(KeyLifetimeCost, _lifetimeCost.ToString("G"));
_persistence.SetValue(KeyLifetimeOperations, _lifetimeOperationCount.ToString());
_persistence.SetValue(KeyLifetimeInputTokens, _lifetimeInputTokens.ToString());
_persistence.SetValue(KeyLifetimeOutputTokens, _lifetimeOutputTokens.ToString());
}
catch
{
// Silently fail - persistence is optional
}
}
public void LoadState()
{
if (_persistence == null) return;
try
{
var costStr = _persistence.GetValue(KeyLifetimeCost);
if (!string.IsNullOrEmpty(costStr) && decimal.TryParse(costStr, out var cost))
{
_lifetimeCost = cost;
}
var opsStr = _persistence.GetValue(KeyLifetimeOperations);
if (!string.IsNullOrEmpty(opsStr) && int.TryParse(opsStr, out var ops))
{
_lifetimeOperationCount = ops;
}
var inputStr = _persistence.GetValue(KeyLifetimeInputTokens);
if (!string.IsNullOrEmpty(inputStr) && int.TryParse(inputStr, out var input))
{
_lifetimeInputTokens = input;
}
var outputStr = _persistence.GetValue(KeyLifetimeOutputTokens);
if (!string.IsNullOrEmpty(outputStr) && int.TryParse(outputStr, out var output))
{
_lifetimeOutputTokens = output;
}
}
catch
{
// Silently fail - start fresh if persistence fails
}
}
}

View File

@@ -0,0 +1,49 @@
using System.Text.Json;
using MarketAlly.GitCommitEditor.Models;
namespace MarketAlly.GitCommitEditor.Services;
public sealed class FileStateRepository : IStateRepository
{
private readonly string _filePath;
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
public FileStateRepository(string filePath)
{
_filePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
}
public async Task<ImproverState> LoadAsync(CancellationToken ct = default)
{
if (!File.Exists(_filePath))
return new ImproverState();
try
{
var json = await File.ReadAllTextAsync(_filePath, ct);
return JsonSerializer.Deserialize<ImproverState>(json) ?? new ImproverState();
}
catch (JsonException)
{
return new ImproverState();
}
catch (IOException)
{
return new ImproverState();
}
}
public async Task SaveAsync(ImproverState state, CancellationToken ct = default)
{
// Ensure directory exists
var directory = Path.GetDirectoryName(_filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
state.LastUpdated = DateTimeOffset.UtcNow;
var json = JsonSerializer.Serialize(state, JsonOptions);
await File.WriteAllTextAsync(_filePath, json, ct);
}
}

312
Services/GitDiagnosticService.cs Executable file
View File

@@ -0,0 +1,312 @@
using System.Text;
using MarketAlly.AIPlugin;
using MarketAlly.AIPlugin.Conversation;
using MarketAlly.GitCommitEditor.Options;
using MarketAlly.GitCommitEditor.Plugins;
using MarketAlly.GitCommitEditor.Resources;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Result of a git diagnosis operation including cost information
/// </summary>
public sealed record DiagnosisOperationResult(
GitDiagnosisResult Diagnosis,
int InputTokens = 0,
int OutputTokens = 0,
decimal EstimatedCost = 0);
/// <summary>
/// Service for diagnosing git issues using AI
/// </summary>
public interface IGitDiagnosticService
{
/// <summary>
/// Diagnose a git issue based on the current repository state
/// </summary>
Task<GitDiagnosisResult> DiagnoseAsync(string repoPath, string? errorMessage = null, CancellationToken ct = default);
/// <summary>
/// Diagnose a git issue and return cost information
/// </summary>
Task<DiagnosisOperationResult> DiagnoseWithCostAsync(string repoPath, string? errorMessage = null, CancellationToken ct = default);
}
/// <summary>
/// AI-powered git diagnostic service using MarketAlly.AIPlugin
/// </summary>
public sealed class GitDiagnosticService : IGitDiagnosticService, IDisposable
{
private const string OperationType = "GitDiagnosis";
private readonly AiOptions _options;
private readonly AIConversation _conversation;
private readonly ICostTrackingService? _costTracker;
public GitDiagnosticService(AiOptions options, ICostTrackingService? costTracker = null)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_costTracker = costTracker;
var provider = ParseProvider(options.Provider);
_conversation = AIConversationBuilder.Create()
.UseProvider(provider, options.ApiKey)
.UseModel(options.Model)
.WithSystemPrompt(BuildSystemPrompt())
.WithTemperature(0.2) // Low temperature for consistent diagnostic output
.WithMaxTokens(options.MaxTokens)
.WithToolExecutionLimit(3)
.RegisterPlugin<ReturnGitDiagnosisPlugin>()
.Build();
}
private static AIProvider ParseProvider(string? providerName)
{
return providerName?.ToLowerInvariant() switch
{
"claude" or "anthropic" => AIProvider.Claude,
"openai" or "gpt" => AIProvider.OpenAI,
"gemini" or "google" => AIProvider.Gemini,
"mistral" => AIProvider.Mistral,
"qwen" or "alibaba" => AIProvider.Qwen,
_ => AIProvider.Claude
};
}
private static string BuildSystemPrompt()
{
return """
You are a git expert who diagnoses and fixes git issues. When given git status, log, and error information, you analyze the problem and provide a clear diagnosis with the exact commands to fix it.
COMMON ISSUES YOU CAN DIAGNOSE:
- Diverged branches (local and remote have different commits)
- Unrelated histories (refusing to merge unrelated histories)
- Merge conflicts
- Detached HEAD state
- Failed rebases
- Uncommitted changes blocking operations
- Authentication issues
- Remote tracking issues
- Corrupt repository state
RULES:
1. Always provide the exact command(s) needed to fix the issue
2. Warn about any potential data loss
3. Explain WHY the issue happened so the user can avoid it
4. If multiple solutions exist, recommend the safest one
5. Set appropriate risk level: low (safe), medium (some risk), high (potential data loss)
When you receive git information, analyze it and call ReturnGitDiagnosis with your findings.
""";
}
public async Task<GitDiagnosisResult> DiagnoseAsync(string repoPath, string? errorMessage = null, CancellationToken ct = default)
{
var result = await DiagnoseWithCostAsync(repoPath, errorMessage, ct);
return result.Diagnosis;
}
public async Task<DiagnosisOperationResult> DiagnoseWithCostAsync(string repoPath, string? errorMessage = null, CancellationToken ct = default)
{
var gitInfo = await GatherGitInfoAsync(repoPath);
var prompt = BuildPrompt(gitInfo, errorMessage);
var response = await _conversation.SendForStructuredOutputAsync<GitDiagnosisResult>(
prompt,
"ReturnGitDiagnosis",
ct);
// Extract cost information
var inputTokens = response.InputTokens;
var outputTokens = response.OutputTokens;
var cost = response.EstimatedCost ?? 0m;
// Record cost if tracking service is available
_costTracker?.RecordOperation(OperationType, inputTokens, outputTokens, cost);
if (response.StructuredOutput is GitDiagnosisResult result)
{
return new DiagnosisOperationResult(result, inputTokens, outputTokens, cost);
}
// Fallback if structured output failed
var fallback = new GitDiagnosisResult
{
Problem = "Unable to diagnose the issue",
Cause = response.FinalMessage ?? Str.Service_AiAnalysisFailed,
FixCommand = "git status",
Warning = "Please review the git state manually",
RiskLevel = RiskLevel.Low
};
return new DiagnosisOperationResult(fallback, inputTokens, outputTokens, cost);
}
private async Task<GitInfo> GatherGitInfoAsync(string repoPath)
{
var info = new GitInfo();
try
{
// Get git status
info.Status = await RunGitCommandAsync(repoPath, "status");
// Get recent log
info.Log = await RunGitCommandAsync(repoPath, "log --oneline -10");
// Get branch info
info.BranchInfo = await RunGitCommandAsync(repoPath, "branch -vv");
// Get remote info
info.RemoteInfo = await RunGitCommandAsync(repoPath, "remote -v");
// Check for ongoing operations
info.OngoingOps = await CheckOngoingOperationsAsync(repoPath);
}
catch (Exception ex)
{
info.Error = ex.Message;
}
return info;
}
private static async Task<string> RunGitCommandAsync(string repoPath, string arguments)
{
try
{
var startInfo = new System.Diagnostics.ProcessStartInfo
{
FileName = "git",
Arguments = arguments,
WorkingDirectory = repoPath,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = System.Diagnostics.Process.Start(startInfo);
if (process == null) return "[Failed to start git]";
var output = await process.StandardOutput.ReadToEndAsync();
var error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
return string.IsNullOrEmpty(error) ? output : $"{output}\n{error}";
}
catch (Exception ex)
{
return $"[Error: {ex.Message}]";
}
}
private static async Task<string> CheckOngoingOperationsAsync(string repoPath)
{
var ops = new List<string>();
var gitDir = Path.Combine(repoPath, ".git");
if (!Directory.Exists(gitDir))
{
// Might be a worktree, try to find the git dir
var gitFile = Path.Combine(repoPath, ".git");
if (File.Exists(gitFile))
{
var content = await File.ReadAllTextAsync(gitFile);
if (content.StartsWith("gitdir:"))
{
gitDir = content.Substring(7).Trim();
}
}
}
if (Directory.Exists(gitDir))
{
if (Directory.Exists(Path.Combine(gitDir, "rebase-merge")) ||
Directory.Exists(Path.Combine(gitDir, "rebase-apply")))
{
ops.Add("REBASE IN PROGRESS");
}
if (File.Exists(Path.Combine(gitDir, "MERGE_HEAD")))
{
ops.Add("MERGE IN PROGRESS");
}
if (File.Exists(Path.Combine(gitDir, "CHERRY_PICK_HEAD")))
{
ops.Add("CHERRY-PICK IN PROGRESS");
}
if (File.Exists(Path.Combine(gitDir, "REVERT_HEAD")))
{
ops.Add("REVERT IN PROGRESS");
}
if (File.Exists(Path.Combine(gitDir, "BISECT_LOG")))
{
ops.Add("BISECT IN PROGRESS");
}
}
return ops.Count > 0 ? string.Join(", ", ops) : "None";
}
private static string BuildPrompt(GitInfo info, string? errorMessage)
{
var sb = new StringBuilder();
sb.AppendLine("Please diagnose this git issue and provide the fix:");
sb.AppendLine();
if (!string.IsNullOrEmpty(errorMessage))
{
sb.AppendLine("ERROR MESSAGE:");
sb.AppendLine(errorMessage);
sb.AppendLine();
}
sb.AppendLine("GIT STATUS:");
sb.AppendLine(info.Status);
sb.AppendLine();
sb.AppendLine("RECENT COMMITS:");
sb.AppendLine(info.Log);
sb.AppendLine();
sb.AppendLine("BRANCH INFO:");
sb.AppendLine(info.BranchInfo);
sb.AppendLine();
sb.AppendLine("REMOTE INFO:");
sb.AppendLine(info.RemoteInfo);
sb.AppendLine();
sb.AppendLine($"ONGOING OPERATIONS: {info.OngoingOps}");
if (!string.IsNullOrEmpty(info.Error))
{
sb.AppendLine();
sb.AppendLine($"ADDITIONAL ERROR: {info.Error}");
}
sb.AppendLine();
sb.AppendLine("Call ReturnGitDiagnosis with the problem, cause, fix command, any warnings, and risk level.");
return sb.ToString();
}
public void Dispose()
{
// AIConversation handles its own disposal
}
private class GitInfo
{
public string Status { get; set; } = string.Empty;
public string Log { get; set; } = string.Empty;
public string BranchInfo { get; set; } = string.Empty;
public string RemoteInfo { get; set; } = string.Empty;
public string OngoingOps { get; set; } = string.Empty;
public string Error { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,759 @@
using System.Text;
using MarketAlly.GitCommitEditor.Models;
using MarketAlly.GitCommitEditor.Models.HistoryHealth;
using MarketAlly.GitCommitEditor.Options;
using MarketAlly.GitCommitEditor.Resources;
using MarketAlly.GitCommitEditor.Rewriters;
namespace MarketAlly.GitCommitEditor.Services;
public sealed class GitMessageImproverService : IGitMessageImproverService
{
private readonly GitImproverOptions _options;
private readonly IGitOperationsService _gitOps;
private readonly ICommitMessageAnalyzer _analyzer;
private readonly ICommitMessageRewriter _rewriter;
private readonly IStateRepository _stateRepo;
private readonly IHistoryHealthAnalyzer _healthAnalyzer;
private readonly IHealthReportGenerator _reportGenerator;
private readonly ICleanupExecutor _cleanupExecutor;
private ImproverState _state;
private bool _disposed;
public GitMessageImproverService(
GitImproverOptions options,
IGitOperationsService gitOps,
ICommitMessageAnalyzer analyzer,
ICommitMessageRewriter rewriter,
IStateRepository stateRepo,
IHistoryHealthAnalyzer? healthAnalyzer = null,
IHealthReportGenerator? reportGenerator = null,
ICleanupExecutor? cleanupExecutor = null)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(gitOps);
ArgumentNullException.ThrowIfNull(analyzer);
ArgumentNullException.ThrowIfNull(rewriter);
ArgumentNullException.ThrowIfNull(stateRepo);
_options = options;
_gitOps = gitOps;
_analyzer = analyzer;
_rewriter = rewriter;
_stateRepo = stateRepo;
// Create default health analysis components if not provided
var commitAnalyzer = new CommitAnalyzer(options.Rules);
_healthAnalyzer = healthAnalyzer ?? new HistoryHealthAnalyzer(commitAnalyzer);
_reportGenerator = reportGenerator ?? new HealthReportGenerator();
_cleanupExecutor = cleanupExecutor ?? new CleanupExecutor(gitOps, analyzer, rewriter);
_state = new ImproverState();
}
/// <summary>
/// Creates a service with default implementations. For simple usage without DI.
/// </summary>
public static async Task<GitMessageImproverService> CreateAsync(GitImproverOptions options)
{
options.ValidateAndThrow();
var gitOps = new GitOperationsService();
var analyzer = new CommitMessageAnalyzer(options.Rules);
var stateRepo = new FileStateRepository(options.StateFilePath);
// Use DynamicCommitRewriter which handles API key changes at runtime
ICommitMessageRewriter rewriter = new DynamicCommitRewriter(options.Ai);
var service = new GitMessageImproverService(options, gitOps, analyzer, rewriter, stateRepo);
await service.LoadStateAsync();
return service;
}
public async Task LoadStateAsync(CancellationToken ct = default)
{
_state = await _stateRepo.LoadAsync(ct);
// Prune old history entries to prevent unbounded growth
var pruned = _state.PruneHistory();
var orphaned = _state.RemoveOrphanedHistory();
if (pruned > 0 || orphaned > 0)
{
await _stateRepo.SaveAsync(_state, ct);
}
}
public IReadOnlyList<ManagedRepo> Repos => _state.Repos;
public IReadOnlyList<RewriteOperation> History => _state.History;
public async Task<IReadOnlyList<ManagedRepo>> ScanAndRegisterReposAsync(CancellationToken ct = default)
{
System.Diagnostics.Debug.WriteLine($"[GitMessageImproverService] Scanning WorkspaceRoot: {_options.WorkspaceRoot}");
var discovered = _gitOps.DiscoverRepositories(_options.WorkspaceRoot);
var newRepos = new List<ManagedRepo>();
foreach (var repoPath in discovered)
{
if (ct.IsCancellationRequested) break;
if (_state.Repos.Any(r => r.Path.Equals(repoPath, StringComparison.OrdinalIgnoreCase)))
continue;
try
{
var managed = _gitOps.CreateManagedRepo(repoPath);
_state.Repos.Add(managed);
newRepos.Add(managed);
}
catch
{
// Skip repos that can't be opened
}
}
await _stateRepo.SaveAsync(_state, ct);
return newRepos;
}
public async Task<ManagedRepo> RegisterRepoAsync(string repoPath)
{
var existing = _state.Repos.FirstOrDefault(r =>
r.Path.Equals(repoPath, StringComparison.OrdinalIgnoreCase));
if (existing != null)
return existing;
var managed = _gitOps.CreateManagedRepo(repoPath);
_state.Repos.Add(managed);
await _stateRepo.SaveAsync(_state);
return managed;
}
public async Task<bool> UnregisterRepoAsync(string repoIdOrPath)
{
var repo = _state.Repos.FirstOrDefault(r =>
r.Id == repoIdOrPath ||
r.Path.Equals(repoIdOrPath, StringComparison.OrdinalIgnoreCase));
if (repo == null) return false;
_state.Repos.Remove(repo);
await _stateRepo.SaveAsync(_state);
return true;
}
public IEnumerable<BranchInfo> GetBranches(string repoPath)
{
return _gitOps.GetBranches(repoPath);
}
public Task<bool> CheckoutBranchAsync(ManagedRepo repo, string branchName)
{
return Task.Run(() =>
{
try
{
using var repository = new LibGit2Sharp.Repository(repo.Path);
// Find the branch
var branch = repository.Branches[branchName]
?? repository.Branches[$"refs/heads/{branchName}"];
if (branch == null)
{
return false;
}
// Check for uncommitted changes
var status = repository.RetrieveStatus(new LibGit2Sharp.StatusOptions());
if (status.IsDirty)
{
return false;
}
// Checkout the branch
LibGit2Sharp.Commands.Checkout(repository, branch);
return true;
}
catch
{
return false;
}
});
}
public IEnumerable<BackupBranchInfo> GetBackupBranches(string repoPath)
{
return _gitOps.GetBackupBranches(repoPath);
}
public bool DeleteBranch(string repoPath, string branchName)
{
return _gitOps.DeleteBranch(repoPath, branchName);
}
public int DeleteAllBackupBranches(string repoPath)
{
var backupBranches = _gitOps.GetBackupBranches(repoPath).ToList();
var deletedCount = 0;
foreach (var branch in backupBranches)
{
if (_gitOps.DeleteBranch(repoPath, branch.Name))
{
deletedCount++;
}
}
return deletedCount;
}
public async Task<IReadOnlyList<CommitAnalysis>> AnalyzeAllReposAsync(
bool onlyNeedsImprovement = true,
IProgress<(string Repo, int Processed)>? progress = null,
CancellationToken ct = default)
{
var allAnalyses = new List<CommitAnalysis>();
foreach (var repo in _state.Repos)
{
if (ct.IsCancellationRequested) break;
var analyses = AnalyzeRepo(repo);
if (onlyNeedsImprovement)
{
analyses = analyses.Where(a => a.Quality.NeedsImprovement);
}
var list = analyses.ToList();
allAnalyses.AddRange(list);
repo.LastScannedAt = DateTimeOffset.UtcNow;
repo.LastAnalyzedAt = DateTimeOffset.UtcNow;
repo.CommitsNeedingImprovement = list.Count(a => a.Quality.NeedsImprovement);
progress?.Report((repo.Name, list.Count));
}
await _stateRepo.SaveAsync(_state, ct);
return allAnalyses;
}
public IEnumerable<CommitAnalysis> AnalyzeRepo(ManagedRepo repo)
{
return _gitOps.AnalyzeCommits(
repo,
_analyzer,
_options.MaxCommitsPerRepo,
_options.AnalyzeSince,
_options.ExcludedAuthors);
}
public CommitAnalysis AnalyzeCommit(string repoPath, string commitHash)
{
var repo = _state.Repos.FirstOrDefault(r =>
r.Path.Equals(repoPath, StringComparison.OrdinalIgnoreCase))
?? _gitOps.CreateManagedRepo(repoPath);
return _gitOps.AnalyzeCommits(repo, _analyzer, 1000)
.First(c => c.CommitHash.StartsWith(commitHash, StringComparison.OrdinalIgnoreCase));
}
public async Task UpdateRepoAnalysisAsync(ManagedRepo repo, int totalCommits, int commitsNeedingImprovement, CancellationToken ct = default)
{
repo.LastAnalyzedAt = DateTimeOffset.UtcNow;
repo.LastScannedAt = DateTimeOffset.UtcNow;
repo.TotalCommits = totalCommits;
repo.CommitsNeedingImprovement = commitsNeedingImprovement;
await _stateRepo.SaveAsync(_state, ct);
}
public async Task<BatchSuggestionResult> GenerateSuggestionsAsync(
IEnumerable<CommitAnalysis> analyses,
IProgress<int>? progress = null,
CancellationToken ct = default)
{
var results = await _rewriter.SuggestBatchAsync(analyses, progress, ct);
var failures = new List<SuggestionFailure>();
var successCount = 0;
foreach (var (analysis, result) in results)
{
analysis.Status = AnalysisStatus.Analyzed;
if (result.Success && !string.IsNullOrEmpty(result.Suggestion))
{
analysis.SuggestedMessage = result.Suggestion;
successCount++;
}
else
{
// Don't set SuggestedMessage for failures - leave it empty
failures.Add(new SuggestionFailure
{
CommitHash = analysis.ShortHash,
OriginalMessage = analysis.OriginalMessage,
Reason = result.ErrorMessage ?? Str.Service_UnknownError,
RawResponse = result.RawResponse
});
}
}
return new BatchSuggestionResult
{
Analyses = results.Select(r => r.Analysis).ToList(),
SuccessCount = successCount,
FailedCount = failures.Count,
Failures = failures
};
}
public async Task<SuggestionResult> GenerateSuggestionAsync(CommitAnalysis analysis, CancellationToken ct = default)
{
var result = await _rewriter.SuggestImprovedMessageAsync(analysis, ct);
analysis.Status = AnalysisStatus.Analyzed;
if (result.Success && !string.IsNullOrEmpty(result.Suggestion))
{
analysis.SuggestedMessage = result.Suggestion;
}
return result;
}
public IReadOnlyList<RewriteOperation> PreviewChanges(IEnumerable<CommitAnalysis> analyses)
{
return analyses
.Where(a => !string.IsNullOrEmpty(a.SuggestedMessage))
.Select(a => new RewriteOperation
{
RepoId = a.RepoId,
RepoPath = a.RepoPath,
CommitHash = a.CommitHash,
OriginalMessage = a.OriginalMessage,
NewMessage = a.SuggestedMessage!,
IsLatestCommit = a.IsLatestCommit,
Status = OperationStatus.Pending
})
.ToList();
}
public async Task<BatchResult> ApplyChangesAsync(
IEnumerable<RewriteOperation> operations,
bool dryRun = true,
IProgress<(int Processed, int Total)>? progress = null,
CancellationToken ct = default)
{
var opList = operations.ToList();
var results = new List<RewriteOperation>();
var processed = 0;
var successful = 0;
var failed = 0;
var skipped = 0;
var byRepo = opList.GroupBy(o => o.RepoPath);
foreach (var repoGroup in byRepo)
{
if (ct.IsCancellationRequested) break;
var repo = _state.Repos.FirstOrDefault(r => r.Path == repoGroup.Key);
if (repo == null)
{
foreach (var op in repoGroup)
{
op.Status = OperationStatus.Failed;
op.ErrorMessage = Str.Service_RepoNotRegistered;
results.Add(op);
failed++;
processed++;
}
continue;
}
var orderedOps = repoGroup.OrderBy(o => o.IsLatestCommit ? 1 : 0).ToList();
foreach (var op in orderedOps)
{
if (ct.IsCancellationRequested)
{
op.Status = OperationStatus.Pending;
skipped++;
results.Add(op);
continue;
}
if (dryRun)
{
op.Status = OperationStatus.Pending;
results.Add(op);
skipped++;
}
else
{
var result = op.IsLatestCommit
? _gitOps.AmendLatestCommit(repo, op.NewMessage)
: _gitOps.RewordOlderCommit(repo, op.CommitHash, op.NewMessage);
op.NewCommitHash = result.NewCommitHash;
op.Status = result.Status;
op.ErrorMessage = result.ErrorMessage;
op.AppliedAt = result.AppliedAt;
results.Add(op);
_state.History.Add(op);
if (result.Status == OperationStatus.Applied)
successful++;
else
failed++;
}
processed++;
progress?.Report((processed, opList.Count));
}
}
await _stateRepo.SaveAsync(_state, ct);
return new BatchResult
{
TotalProcessed = processed,
Successful = successful,
Failed = failed,
Skipped = skipped,
Operations = results
};
}
public async Task<RewriteOperation> ApplyChangeAsync(CommitAnalysis analysis, CancellationToken ct = default)
{
if (string.IsNullOrEmpty(analysis.SuggestedMessage))
throw new InvalidOperationException(Str.Service_NoSuggestion);
var repo = _state.Repos.FirstOrDefault(r => r.Id == analysis.RepoId)
?? throw new InvalidOperationException(Str.Service_RepoNotFound(analysis.RepoId));
var operation = analysis.IsLatestCommit
? _gitOps.AmendLatestCommit(repo, analysis.SuggestedMessage)
: _gitOps.RewordOlderCommit(repo, analysis.CommitHash, analysis.SuggestedMessage);
_state.History.Add(operation);
analysis.Status = operation.Status == OperationStatus.Applied
? AnalysisStatus.Applied
: AnalysisStatus.Failed;
await _stateRepo.SaveAsync(_state, ct);
return operation;
}
public bool UndoCommitAmend(string repoPath, string originalCommitHash)
{
return _gitOps.UndoCommitAmend(repoPath, originalCommitHash);
}
public bool IsCommitPushed(string repoPath, string commitHash)
{
return _gitOps.IsCommitPushed(repoPath, commitHash);
}
public TrackingInfo GetTrackingInfo(string repoPath)
{
return _gitOps.GetTrackingInfo(repoPath);
}
public GitPushResult ForcePush(string repoPath)
{
return _gitOps.ForcePush(repoPath);
}
public GitPushResult Push(string repoPath)
{
return _gitOps.Push(repoPath);
}
public string GenerateSummaryReport()
{
var sb = new StringBuilder();
sb.AppendLine("═══════════════════════════════════════════════════════════════");
sb.AppendLine("GIT MESSAGE IMPROVER - SUMMARY REPORT");
sb.AppendLine($"Generated: {DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine("═══════════════════════════════════════════════════════════════");
sb.AppendLine();
sb.AppendLine($"Total Repositories: {_state.Repos.Count}");
sb.AppendLine($"Total Operations in History: {_state.History.Count}");
sb.AppendLine();
foreach (var repo in _state.Repos.OrderBy(r => r.Name))
{
sb.AppendLine($"┌─ {repo.Name}");
sb.AppendLine($"│ Path: {repo.Path}");
sb.AppendLine($"│ Branch: {repo.CurrentBranch}");
sb.AppendLine($"│ Last Scanned: {repo.LastScannedAt?.ToString("yyyy-MM-dd HH:mm") ?? "Never"}");
sb.AppendLine($"│ Commits Needing Improvement: {repo.CommitsNeedingImprovement}");
sb.AppendLine($"└────────────────────────────────────────");
sb.AppendLine();
}
var recentOps = _state.History
.OrderByDescending(h => h.CreatedAt)
.Take(10);
if (recentOps.Any())
{
sb.AppendLine("RECENT OPERATIONS:");
sb.AppendLine("─────────────────────────────────────────");
foreach (var op in recentOps)
{
var status = op.Status switch
{
OperationStatus.Applied => "✓",
OperationStatus.Failed => "✗",
_ => "○"
};
sb.AppendLine($"{status} [{op.CreatedAt:MM-dd HH:mm}] {op.CommitHash[..7]}: {op.Status}");
}
}
return sb.ToString();
}
// IHistoryHealthService implementation
public async Task<HistoryHealthReport> AnalyzeHistoryHealthAsync(
string repoPath,
HistoryAnalysisOptions? options = null,
IProgress<AnalysisProgress>? progress = null,
CancellationToken ct = default)
{
var analysis = await _healthAnalyzer.AnalyzeAsync(repoPath, options, progress, ct);
return _reportGenerator.GenerateReport(analysis);
}
public async Task<HistoryHealthReport> AnalyzeHistoryHealthAsync(
ManagedRepo repo,
HistoryAnalysisOptions? options = null,
IProgress<AnalysisProgress>? progress = null,
CancellationToken ct = default)
{
var analysis = await _healthAnalyzer.AnalyzeAsync(repo, options, progress, ct);
return _reportGenerator.GenerateReport(analysis);
}
public Task<string> ExportHealthReportAsync(
HistoryHealthReport report,
ReportFormat format,
CancellationToken ct = default)
{
return _reportGenerator.ExportReportAsync(report, format, ct);
}
public Task ExportHealthReportToFileAsync(
HistoryHealthReport report,
ReportFormat format,
string outputPath,
CancellationToken ct = default)
{
return _reportGenerator.ExportReportToFileAsync(report, format, outputPath, ct);
}
public Task<CleanupExecutionResult> ExecuteCleanupAsync(
ManagedRepo repo,
CleanupOperation operation,
CleanupExecutionOptions? options = null,
IProgress<CleanupProgress>? progress = null,
CancellationToken ct = default)
{
return _cleanupExecutor.ExecuteAsync(repo, operation, options, progress, ct);
}
public async Task<BatchCleanupResult> ExecuteAllCleanupsAsync(
ManagedRepo repo,
CleanupSuggestions suggestions,
CleanupExecutionOptions? options = null,
IProgress<CleanupProgress>? progress = null,
CancellationToken ct = default)
{
// Execute operations in order: automated first, then semi-automated
// Skip manual operations as they require human intervention
var operations = suggestions.AutomatedOperations
.Concat(suggestions.SemiAutomatedOperations)
.ToList();
return await _cleanupExecutor.ExecuteBatchAsync(repo, operations, options, progress, ct);
}
public Task<string> CreateBackupBranchAsync(
ManagedRepo repo,
string? branchName = null,
CancellationToken ct = default)
{
return _cleanupExecutor.CreateBackupBranchAsync(repo, branchName, ct);
}
public RewriteSafetyInfo GetRewriteSafetyInfo(string repoPath, IEnumerable<CommitAnalysis> commits)
{
var commitList = commits.ToList();
if (!commitList.Any())
{
return new RewriteSafetyInfo
{
TotalCommitCount = 0,
LocalOnlyCommitCount = 0,
PushedCommitCount = 0
};
}
// Check for uncommitted changes
bool hasUncommittedChanges;
using (var repo = new LibGit2Sharp.Repository(repoPath))
{
var status = repo.RetrieveStatus(new LibGit2Sharp.StatusOptions());
hasUncommittedChanges = status.IsDirty;
}
// Get tracking info
var trackingInfo = _gitOps.GetTrackingInfo(repoPath);
// Check which commits have been pushed
var pushedCount = 0;
var localCount = 0;
foreach (var commit in commitList)
{
if (_gitOps.IsCommitPushed(repoPath, commit.CommitHash))
{
pushedCount++;
}
else
{
localCount++;
}
}
return new RewriteSafetyInfo
{
HasUncommittedChanges = hasUncommittedChanges,
HasPushedCommits = pushedCount > 0,
PushedCommitCount = pushedCount,
LocalOnlyCommitCount = localCount,
TotalCommitCount = commitList.Count,
HasRemoteTracking = trackingInfo.HasUpstream,
RemoteTrackingBranch = trackingInfo.UpstreamBranch,
AheadOfRemote = trackingInfo.AheadBy,
BehindRemote = trackingInfo.BehindBy
};
}
public async Task<BatchRewriteResult> ExecuteBatchRewriteAsync(
string repoPath,
IEnumerable<CommitAnalysis> commits,
bool createBackup = true,
IProgress<(int Current, int Total, string CommitHash)>? progress = null,
CancellationToken ct = default)
{
var commitList = commits
.Where(c => !string.IsNullOrEmpty(c.SuggestedMessage))
.ToList();
if (!commitList.Any())
{
return BatchRewriteResult.Failure("No commits with suggestions to apply.");
}
// Get safety info
var safetyInfo = GetRewriteSafetyInfo(repoPath, commitList);
// Block if uncommitted changes
if (safetyInfo.HasUncommittedChanges)
{
return BatchRewriteResult.Failure(Str.Service_UncommittedChanges);
}
// Get the managed repo
var repo = _state.Repos.FirstOrDefault(r => r.Path.Equals(repoPath, StringComparison.OrdinalIgnoreCase));
if (repo == null)
{
return BatchRewriteResult.Failure(Str.Service_RepoNotRegisteredPath(repoPath));
}
string? backupBranch = null;
// Create backup branch if requested
if (createBackup)
{
try
{
backupBranch = await CreateBackupBranchAsync(repo, null, ct);
}
catch (Exception ex)
{
return BatchRewriteResult.Failure($"Failed to create backup branch: {ex.Message}");
}
}
var operations = new List<RewriteOperation>();
var successCount = 0;
var failedCount = 0;
var total = commitList.Count;
// Process ALL commits (including HEAD) in a single batch operation
// This ensures consistent rewriting without stale references
System.Diagnostics.Debug.WriteLine($"[BatchRewrite] Processing {commitList.Count} commits in single batch");
var rewrites = commitList.ToDictionary(c => c.CommitHash, c => c.SuggestedMessage!);
var batchOperations = _gitOps.RewordMultipleCommits(repo, rewrites);
foreach (var operation in batchOperations)
{
var commit = commitList.First(c => c.CommitHash == operation.CommitHash);
progress?.Report((operations.Count + 1, total, commit.CommitHash[..7]));
System.Diagnostics.Debug.WriteLine($"[BatchRewrite] Commit {commit.CommitHash[..7]}: Status={operation.Status}, NewHash={operation.NewCommitHash?[..7] ?? "null"}");
operations.Add(operation);
_state.History.Add(operation);
if (operation.Status == OperationStatus.Applied)
{
successCount++;
commit.Status = AnalysisStatus.Applied;
}
else
{
failedCount++;
commit.Status = AnalysisStatus.Failed;
System.Diagnostics.Debug.WriteLine($"[BatchRewrite] FAILED: {operation.ErrorMessage}");
}
}
// Invalidate the repository cache to ensure subsequent analysis sees the new commits
_gitOps.InvalidateCache(repoPath);
System.Diagnostics.Debug.WriteLine($"[BatchRewrite] Cache invalidated for {repoPath}");
await _stateRepo.SaveAsync(_state, ct);
return new BatchRewriteResult
{
Success = successCount > 0,
SuccessCount = successCount,
FailedCount = failedCount,
SkippedCount = total - successCount - failedCount,
RequiresForcePush = safetyInfo.HasPushedCommits && successCount > 0,
BackupBranchName = backupBranch,
Operations = operations
};
}
public void Dispose()
{
if (_disposed) return;
_gitOps.Dispose();
(_rewriter as IDisposable)?.Dispose();
_disposed = true;
}
}

1000
Services/GitOperationsService.cs Executable file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,761 @@
using System.Text;
using System.Text.Json;
using MarketAlly.GitCommitEditor.Models.HistoryHealth;
using MarketAlly.GitCommitEditor.Resources;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Generates health reports and calculates scores.
/// </summary>
public sealed class HealthReportGenerator : IHealthReportGenerator
{
private readonly HealthScoringWeights _weights;
public HealthReportGenerator(HealthScoringWeights? weights = null)
{
_weights = weights ?? HealthScoringWeights.Default;
}
public HistoryHealthReport GenerateReport(HistoryHealthAnalysis analysis)
{
var score = CalculateScore(analysis);
var issues = DetectIssues(analysis);
var recommendations = GenerateRecommendations(issues, analysis);
var cleanup = GenerateCleanupSuggestions(issues, analysis);
return new HistoryHealthReport
{
RepoId = analysis.RepoPath,
RepoName = analysis.RepoName,
RepoPath = analysis.RepoPath,
CurrentBranch = analysis.CurrentBranch,
CommitsAnalyzed = analysis.CommitsAnalyzed,
Score = score,
DuplicateMetrics = analysis.Duplicates,
MergeMetrics = analysis.MergeMetrics,
BranchMetrics = analysis.BranchMetrics,
MessageDistribution = analysis.MessageDistribution,
AuthorshipMetrics = analysis.AuthorshipMetrics,
Issues = issues,
Recommendations = recommendations,
CleanupSuggestions = cleanup
};
}
private HealthScore CalculateScore(HistoryHealthAnalysis analysis)
{
var duplicateScore = CalculateDuplicateScore(analysis.Duplicates);
var mergeScore = CalculateMergeScore(analysis.MergeMetrics);
var branchScore = CalculateBranchScore(analysis.BranchMetrics);
var messageScore = CalculateMessageScore(analysis.MessageDistribution);
var authorshipScore = CalculateAuthorshipScore(analysis.AuthorshipMetrics);
var overallScore = (int)Math.Round(
duplicateScore * _weights.DuplicateWeight +
mergeScore * _weights.MergeWeight +
branchScore * _weights.BranchWeight +
messageScore * _weights.MessageWeight +
authorshipScore * _weights.AuthorshipWeight
);
overallScore = Math.Clamp(overallScore, 0, 100);
return new HealthScore
{
OverallScore = overallScore,
Grade = HealthGradeExtensions.FromScore(overallScore),
ComponentScores = new ComponentScores
{
DuplicateScore = duplicateScore,
MergeScore = mergeScore,
BranchScore = branchScore,
MessageScore = messageScore,
AuthorshipScore = authorshipScore
}
};
}
private int CalculateDuplicateScore(DuplicateCommitMetrics metrics)
{
if (metrics.TotalDuplicateGroups == 0)
return 100;
var duplicateRatio = metrics.DuplicateRatio;
var score = 100 - (int)(duplicateRatio * 10);
var exactPenalty = metrics.ExactDuplicates * 5;
return Math.Max(0, score - exactPenalty);
}
private int CalculateMergeScore(MergeCommitMetrics metrics)
{
var mergeRatio = metrics.MergeRatio;
var baseScore = mergeRatio switch
{
<= 10 => 100,
<= 25 => 90,
<= 35 => 75,
<= 50 => 50,
_ => 25
};
// Additional penalty for merge fix commits
var fixPenalty = Math.Min(20, metrics.MergeFixCommits * 3);
return Math.Max(0, baseScore - fixPenalty);
}
private int CalculateBranchScore(BranchComplexityMetrics metrics)
{
var baseScore = metrics.Topology switch
{
BranchTopologyType.Linear => 100,
BranchTopologyType.Balanced => 90,
BranchTopologyType.GitFlow => 75,
BranchTopologyType.Tangled => 50,
BranchTopologyType.Spaghetti => 25,
_ => 50
};
var stalePenalty = Math.Min(20, metrics.StaleBranches * 2);
var crossMergePenalty = Math.Min(25, metrics.CrossMerges * 2);
return Math.Max(0, baseScore - stalePenalty - crossMergePenalty);
}
private int CalculateMessageScore(MessageQualityDistribution distribution)
{
if (distribution.TotalCommits == 0)
return 100;
var avgScore = (int)distribution.AverageScore;
var poorRatio = (double)distribution.Poor / distribution.TotalCommits;
var poorPenalty = (int)(poorRatio * 30);
return Math.Max(0, avgScore - poorPenalty);
}
private int CalculateAuthorshipScore(AuthorshipMetrics metrics)
{
if (metrics.TotalCommits == 0)
return 100;
var missingEmailRatio = (double)metrics.MissingEmailCount / metrics.TotalCommits;
var invalidEmailRatio = (double)metrics.InvalidEmailCount / metrics.TotalCommits;
var score = 100 - (int)((missingEmailRatio + invalidEmailRatio) * 100);
return Math.Max(0, score);
}
private List<HealthIssue> DetectIssues(HistoryHealthAnalysis analysis)
{
var issues = new List<HealthIssue>();
// Duplicate issues - separate ExactTree (true duplicates) from ExactMessage (just same message)
var exactTreeGroups = analysis.Duplicates.DuplicateGroups
.Where(g => g.Type == DuplicateType.ExactTree)
.ToList();
var exactMessageGroups = analysis.Duplicates.DuplicateGroups
.Where(g => g.Type == DuplicateType.ExactMessage)
.ToList();
// ExactTree duplicates - these are TRUE duplicates that can be safely squashed
if (exactTreeGroups.Count > 0)
{
var exactTreeInstances = exactTreeGroups.Sum(g => g.InstanceCount - 1);
var severity = exactTreeInstances > 5
? HealthIssueSeverity.Error
: exactTreeGroups.Count > 2
? HealthIssueSeverity.Warning
: HealthIssueSeverity.Info;
issues.Add(new HealthIssue
{
Code = "DUPLICATE_COMMITS",
Category = "Duplicates",
Severity = severity,
Title = Str.Report_DuplicateContent,
Description = Str.Report_DuplicateContentDesc(exactTreeGroups.Count, exactTreeInstances),
ImpactScore = exactTreeInstances * 5 + exactTreeGroups.Count * 2,
AffectedCommits = exactTreeGroups
.SelectMany(g => g.CommitHashes)
.Take(20)
.ToList()
});
}
// ExactMessage duplicates - these have same message but DIFFERENT code, DO NOT squash
if (exactMessageGroups.Count > 0)
{
var messageInstances = exactMessageGroups.Sum(g => g.InstanceCount);
issues.Add(new HealthIssue
{
Code = "SIMILAR_MESSAGES",
Category = "Messages",
Severity = HealthIssueSeverity.Info,
Title = Str.Report_DuplicateMessages,
Description = Str.Report_DuplicateMessagesDesc(exactMessageGroups.Count, messageInstances),
ImpactScore = exactMessageGroups.Count, // Low impact - just informational
AffectedCommits = exactMessageGroups
.SelectMany(g => g.CommitHashes)
.Take(20)
.ToList()
});
}
// Merge issues
var mergeRatio = analysis.MergeMetrics.MergeRatio;
if (mergeRatio > 35)
{
var severity = mergeRatio > 50
? HealthIssueSeverity.Error
: HealthIssueSeverity.Warning;
issues.Add(new HealthIssue
{
Code = mergeRatio > 50 ? "EXCESSIVE_MERGES" : "HIGH_MERGE_RATIO",
Category = "Merges",
Severity = severity,
Title = mergeRatio > 50 ? Str.Report_ExcessiveMerges : Str.Report_HighMergeRatio,
Description = Str.Report_MergeRatioDesc(mergeRatio, analysis.MergeMetrics.TotalMerges, analysis.MergeMetrics.TotalCommits),
ImpactScore = (mergeRatio - 25) / 2
});
}
// Merge fix commits
if (analysis.MergeMetrics.MergeFixCommits > 0)
{
issues.Add(new HealthIssue
{
Code = "MERGE_FIX_COMMITS",
Category = "Merges",
Severity = HealthIssueSeverity.Warning,
Title = Str.Report_MergeFixCommits,
Description = Str.Report_MergeFixDesc(analysis.MergeMetrics.MergeFixCommits),
ImpactScore = analysis.MergeMetrics.MergeFixCommits * 3,
AffectedCommits = analysis.MergeMetrics.MergeFixCommitHashes.Take(10).ToList()
});
}
// Branch complexity
if (analysis.BranchMetrics.Topology >= BranchTopologyType.Tangled)
{
issues.Add(new HealthIssue
{
Code = "TANGLED_BRANCHES",
Category = "Branches",
Severity = analysis.BranchMetrics.Topology == BranchTopologyType.Spaghetti
? HealthIssueSeverity.Error
: HealthIssueSeverity.Warning,
Title = Str.Report_CrossMerges,
Description = Str.Report_CrossMergesDesc(analysis.BranchMetrics.CrossMerges),
ImpactScore = analysis.BranchMetrics.CrossMerges * 2
});
}
// Stale branches
if (analysis.BranchMetrics.StaleBranches > 3)
{
issues.Add(new HealthIssue
{
Code = "STALE_BRANCHES",
Category = "Branches",
Severity = HealthIssueSeverity.Info,
Title = Str.Report_StaleBranches,
Description = Str.Report_StaleBranchesDesc(analysis.BranchMetrics.StaleBranches),
ImpactScore = analysis.BranchMetrics.StaleBranches
});
}
// Message quality
if (analysis.MessageDistribution.AverageScore < 50)
{
issues.Add(new HealthIssue
{
Code = "LOW_MESSAGE_QUALITY",
Category = "Messages",
Severity = HealthIssueSeverity.Error,
Title = "Low average message quality",
Description = $"Average commit message score is {analysis.MessageDistribution.AverageScore:F0}/100. " +
$"{analysis.MessageDistribution.Poor} commits have poor quality messages.",
ImpactScore = (int)(50 - analysis.MessageDistribution.AverageScore),
AffectedCommits = analysis.MessageDistribution.PoorCommitHashes.Take(20).ToList()
});
}
else if (analysis.MessageDistribution.Poor > analysis.MessageDistribution.TotalCommits * 0.3)
{
issues.Add(new HealthIssue
{
Code = "MANY_POOR_MESSAGES",
Category = "Messages",
Severity = HealthIssueSeverity.Warning,
Title = "Many poor quality messages",
Description = $"{analysis.MessageDistribution.Poor} commits ({analysis.MessageDistribution.Poor * 100 / Math.Max(1, analysis.MessageDistribution.TotalCommits)}%) " +
"have poor quality messages (score < 50).",
ImpactScore = analysis.MessageDistribution.Poor / 2,
AffectedCommits = analysis.MessageDistribution.PoorCommitHashes.Take(20).ToList()
});
}
return issues.OrderByDescending(i => i.Severity).ThenByDescending(i => i.ImpactScore).ToList();
}
private List<HealthRecommendation> GenerateRecommendations(
List<HealthIssue> issues,
HistoryHealthAnalysis analysis)
{
var recommendations = new List<HealthRecommendation>();
foreach (var issue in issues.Where(i => i.Severity >= HealthIssueSeverity.Warning))
{
// ExpectedScoreImprovement should match the issue's ImpactScore
var rec = issue.Code switch
{
"DUPLICATE_COMMITS" => new HealthRecommendation
{
Category = "Duplicates",
Title = "Squash duplicate commits",
Description = "Remove duplicate commits to clean up history",
Action = "Use interactive rebase to squash or drop duplicate commits",
Rationale = "Duplicates make history harder to understand and can cause merge conflicts",
PriorityScore = 80,
Effort = EstimatedEffort.Medium,
ExpectedScoreImprovement = issue.ImpactScore
},
"EXCESSIVE_MERGES" or "HIGH_MERGE_RATIO" => new HealthRecommendation
{
Category = "Workflow",
Title = "Switch to rebase workflow",
Description = "Use rebase instead of merge for feature branches",
Action = "Configure git to use rebase by default: git config pull.rebase true",
Rationale = "Linear history is easier to understand and bisect",
PriorityScore = 70,
Effort = EstimatedEffort.Low,
ExpectedScoreImprovement = issue.ImpactScore
},
"MERGE_FIX_COMMITS" => new HealthRecommendation
{
Category = "Merges",
Title = "Consolidate merge fix commits",
Description = "Squash fix commits into their parent merge",
Action = "Use interactive rebase to combine fix commits with merges",
Rationale = "Fix commits indicate problematic merges that should be cleaned up",
PriorityScore = 60,
Effort = EstimatedEffort.Medium,
ExpectedScoreImprovement = issue.ImpactScore
},
"TANGLED_BRANCHES" => new HealthRecommendation
{
Category = "Branches",
Title = "Linearize branch structure",
Description = "Rebase feature branches onto main instead of cross-merging",
Action = "For future work: always branch from and merge to main only",
Rationale = "Cross-merges create complex dependencies and merge conflicts",
PriorityScore = 65,
Effort = EstimatedEffort.High,
ExpectedScoreImprovement = issue.ImpactScore
},
"STALE_BRANCHES" => new HealthRecommendation
{
Category = "Branches",
Title = "Archive stale branches",
Description = "Delete or tag old branches that are no longer needed",
Action = "git branch -d <branch> for merged branches, or create archive tags",
Rationale = "Stale branches clutter the repository and can cause confusion",
PriorityScore = 30,
Effort = EstimatedEffort.Minimal,
ExpectedScoreImprovement = issue.ImpactScore
},
"LOW_MESSAGE_QUALITY" or "MANY_POOR_MESSAGES" => new HealthRecommendation
{
Category = "Messages",
Title = "Rewrite poor commit messages",
Description = "Use AI to generate better commit messages",
Action = "Use GitCleaner's AI suggestion feature to reword commits",
Rationale = "Good commit messages are essential for maintainability",
PriorityScore = 75,
Effort = EstimatedEffort.Low,
ExpectedScoreImprovement = issue.ImpactScore
},
_ => null
};
if (rec != null)
recommendations.Add(rec);
}
return recommendations.OrderByDescending(r => r.PriorityScore).ToList();
}
private CleanupSuggestions GenerateCleanupSuggestions(
List<HealthIssue> issues,
HistoryHealthAnalysis analysis)
{
var automated = new List<CleanupOperation>();
var semiAutomated = new List<CleanupOperation>();
var manual = new List<CleanupOperation>();
// Message rewriting - fully automated with existing feature
if (analysis.MessageDistribution.Poor > 0)
{
// ImpactScore for MANY_POOR_MESSAGES = Poor / 2
// ImpactScore for LOW_MESSAGE_QUALITY = (50 - AverageScore)
var messageImpact = Math.Max(
analysis.MessageDistribution.Poor / 2,
(int)Math.Max(0, 50 - analysis.MessageDistribution.AverageScore));
automated.Add(new CleanupOperation
{
Id = "rewrite-messages",
Title = "Rewrite poor commit messages",
Description = $"Use AI to improve {analysis.MessageDistribution.Poor} commit messages with score < 50",
Type = CleanupType.RewordMessages,
AutomationLevel = CleanupAutomationLevel.FullyAutomated,
Effort = EstimatedEffort.Low,
Risk = RiskLevel.Low,
ExpectedScoreImprovement = messageImpact,
AffectedCommits = analysis.MessageDistribution.PoorCommitHashes.ToList()
});
}
// Duplicate squashing - semi-automated
// IMPORTANT: Only squash ExactTree duplicates (identical file content)
// ExactMessage duplicates have the same message but DIFFERENT code changes - squashing would lose code!
var exactTreeGroups = analysis.Duplicates.DuplicateGroups
.Where(g => g.Type == DuplicateType.ExactTree)
.ToList();
if (exactTreeGroups.Count > 0)
{
var exactTreeInstances = exactTreeGroups.Sum(g => g.InstanceCount - 1);
// Impact score based only on exact tree duplicates (safe to squash)
var duplicateImpact = exactTreeInstances * 5 + exactTreeGroups.Count * 2;
semiAutomated.Add(new CleanupOperation
{
Id = "squash-duplicates",
Title = "Squash duplicate commits",
Description = $"Consolidate {exactTreeGroups.Count} duplicate commit groups " +
$"with identical content ({exactTreeInstances} redundant commits)",
Type = CleanupType.SquashDuplicates,
AutomationLevel = CleanupAutomationLevel.SemiAutomated,
Effort = EstimatedEffort.Medium,
Risk = RiskLevel.Medium,
ExpectedScoreImprovement = duplicateImpact,
AffectedCommits = exactTreeGroups
.SelectMany(g => g.CommitHashes)
.ToList(),
GitCommand = "git rebase -i HEAD~N # Mark duplicates as 'drop' or 'fixup'"
});
}
// Stale branch cleanup - semi-automated
if (analysis.BranchMetrics.StaleBranches > 0)
{
// ImpactScore for STALE_BRANCHES = StaleBranches
semiAutomated.Add(new CleanupOperation
{
Id = "archive-stale-branches",
Title = "Archive stale branches",
Description = $"Delete or tag {analysis.BranchMetrics.StaleBranches} stale branches",
Type = CleanupType.ArchiveBranches,
AutomationLevel = CleanupAutomationLevel.SemiAutomated,
Effort = EstimatedEffort.Minimal,
Risk = RiskLevel.None,
ExpectedScoreImprovement = analysis.BranchMetrics.StaleBranches,
GitCommand = "git branch -d <branch> # For merged branches\n" +
"git tag archive/<branch> <branch> && git branch -D <branch> # For archiving"
});
}
// Merge consolidation - semi-automated (we can execute this)
if (analysis.MergeMetrics.MergeFixCommits > 0)
{
// ImpactScore for MERGE_FIX_COMMITS = MergeFixCommits * 3
semiAutomated.Add(new CleanupOperation
{
Id = "consolidate-merges",
Title = "Consolidate merge fix commits",
Description = $"Squash {analysis.MergeMetrics.MergeFixCommits} merge fix commits into their parent merges",
Type = CleanupType.ConsolidateMerges,
AutomationLevel = CleanupAutomationLevel.SemiAutomated,
Effort = EstimatedEffort.Medium,
Risk = RiskLevel.High,
ExpectedScoreImprovement = analysis.MergeMetrics.MergeFixCommits * 3,
AffectedCommits = analysis.MergeMetrics.MergeFixCommitHashes.ToList(),
GitCommand = "git rebase -i <merge-commit>^ # Squash fix commits into merge"
});
}
// History linearization - semi-automated (we can execute this)
if (analysis.BranchMetrics.Topology >= BranchTopologyType.Tangled)
{
semiAutomated.Add(new CleanupOperation
{
Id = "linearize-history",
Title = "Linearize branch structure",
Description = "Remove merge commits and create a cleaner, linear history",
Type = CleanupType.RebaseLinearize,
AutomationLevel = CleanupAutomationLevel.SemiAutomated,
Effort = EstimatedEffort.Medium,
Risk = RiskLevel.High,
ExpectedScoreImprovement = 15,
GitCommand = "git rebase main # Alternative manual approach"
});
}
return new CleanupSuggestions
{
AutomatedOperations = automated,
SemiAutomatedOperations = semiAutomated,
ManualOperations = manual
};
}
public async Task<string> ExportReportAsync(
HistoryHealthReport report,
ReportFormat format,
CancellationToken ct = default)
{
return format switch
{
ReportFormat.Json => ExportToJson(report),
ReportFormat.Markdown => ExportToMarkdown(report),
ReportFormat.Html => ExportToHtml(report),
ReportFormat.Console => ExportToConsole(report),
_ => throw new ArgumentOutOfRangeException(nameof(format))
};
}
public async Task ExportReportToFileAsync(
HistoryHealthReport report,
ReportFormat format,
string outputPath,
CancellationToken ct = default)
{
var content = await ExportReportAsync(report, format, ct);
await File.WriteAllTextAsync(outputPath, content, ct);
}
private string ExportToJson(HistoryHealthReport report)
{
return JsonSerializer.Serialize(report, new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
});
}
private string ExportToMarkdown(HistoryHealthReport report)
{
var sb = new StringBuilder();
sb.AppendLine("# Git History Health Report");
sb.AppendLine();
sb.AppendLine($"**Repository:** {report.RepoName}");
sb.AppendLine($"**Branch:** {report.CurrentBranch}");
sb.AppendLine($"**Generated:** {report.GeneratedAt:yyyy-MM-dd HH:mm:ss} UTC");
sb.AppendLine($"**Commits Analyzed:** {report.CommitsAnalyzed}");
sb.AppendLine();
sb.AppendLine("---");
sb.AppendLine();
// Overall score
var gradeIcon = report.Score.Grade.GetIcon();
sb.AppendLine($"## Overall Health Score: {report.Score.OverallScore}/100 ({report.Score.Grade}) {gradeIcon}");
sb.AppendLine();
sb.AppendLine(report.Score.Grade.GetDescription());
sb.AppendLine();
// Component scores
sb.AppendLine("### Component Scores");
sb.AppendLine();
sb.AppendLine($"| Component | Score | Status |");
sb.AppendLine($"|-----------|-------|--------|");
sb.AppendLine($"| Messages | {report.Score.ComponentScores.MessageScore}/100 | {GetStatusIcon(report.Score.ComponentScores.MessageScore)} |");
sb.AppendLine($"| Merges | {report.Score.ComponentScores.MergeScore}/100 | {GetStatusIcon(report.Score.ComponentScores.MergeScore)} |");
sb.AppendLine($"| Duplicates | {report.Score.ComponentScores.DuplicateScore}/100 | {GetStatusIcon(report.Score.ComponentScores.DuplicateScore)} |");
sb.AppendLine($"| Branches | {report.Score.ComponentScores.BranchScore}/100 | {GetStatusIcon(report.Score.ComponentScores.BranchScore)} |");
sb.AppendLine($"| Authorship | {report.Score.ComponentScores.AuthorshipScore}/100 | {GetStatusIcon(report.Score.ComponentScores.AuthorshipScore)} |");
sb.AppendLine();
// Issues
if (report.Issues.Count > 0)
{
sb.AppendLine("---");
sb.AppendLine();
sb.AppendLine($"## Issues Found ({report.Issues.Count})");
sb.AppendLine();
foreach (var issue in report.Issues)
{
var severityIcon = issue.Severity switch
{
HealthIssueSeverity.Critical => "🚨",
HealthIssueSeverity.Error => "❌",
HealthIssueSeverity.Warning => "⚠️",
_ => ""
};
sb.AppendLine($"### {severityIcon} {issue.Title}");
sb.AppendLine();
sb.AppendLine($"**Code:** `{issue.Code}` | **Category:** {issue.Category} | **Impact:** -{issue.ImpactScore} points");
sb.AppendLine();
sb.AppendLine(issue.Description);
sb.AppendLine();
if (issue.AffectedCommits.Count > 0)
{
sb.AppendLine($"**Affected commits:** `{string.Join("`, `", issue.AffectedCommits.Take(5))}`" +
(issue.AffectedCommits.Count > 5 ? $" and {issue.AffectedCommits.Count - 5} more..." : ""));
sb.AppendLine();
}
}
}
// Recommendations
if (report.Recommendations.Count > 0)
{
sb.AppendLine("---");
sb.AppendLine();
sb.AppendLine("## Recommendations");
sb.AppendLine();
foreach (var rec in report.Recommendations)
{
sb.AppendLine($"### {rec.Title}");
sb.AppendLine();
sb.AppendLine($"**Priority:** {rec.PriorityScore}/100 | **Effort:** {rec.Effort} | **Expected Improvement:** +{rec.ExpectedScoreImprovement} points");
sb.AppendLine();
sb.AppendLine(rec.Description);
sb.AppendLine();
sb.AppendLine($"**Action:** {rec.Action}");
sb.AppendLine();
}
}
// Cleanup suggestions
if (report.CleanupSuggestions != null && report.CleanupSuggestions.TotalOperations > 0)
{
sb.AppendLine("---");
sb.AppendLine();
sb.AppendLine("## Cleanup Operations");
sb.AppendLine();
sb.AppendLine($"**Total expected improvement:** +{report.CleanupSuggestions.TotalExpectedImprovement} points");
sb.AppendLine();
if (report.CleanupSuggestions.AutomatedOperations.Count > 0)
{
sb.AppendLine("### Automated (Safe)");
foreach (var op in report.CleanupSuggestions.AutomatedOperations)
{
sb.AppendLine($"- **{op.Title}**: {op.Description} (+{op.ExpectedScoreImprovement} points)");
}
sb.AppendLine();
}
if (report.CleanupSuggestions.SemiAutomatedOperations.Count > 0)
{
sb.AppendLine("### Semi-Automated (Review Required)");
foreach (var op in report.CleanupSuggestions.SemiAutomatedOperations)
{
sb.AppendLine($"- **{op.Title}**: {op.Description} (+{op.ExpectedScoreImprovement} points)");
if (!string.IsNullOrEmpty(op.GitCommand))
{
sb.AppendLine($" ```bash");
sb.AppendLine($" {op.GitCommand}");
sb.AppendLine($" ```");
}
}
sb.AppendLine();
}
if (report.CleanupSuggestions.ManualOperations.Count > 0)
{
sb.AppendLine("### Manual (High Risk)");
foreach (var op in report.CleanupSuggestions.ManualOperations)
{
sb.AppendLine($"- **{op.Title}**: {op.Description} (+{op.ExpectedScoreImprovement} points)");
if (!string.IsNullOrEmpty(op.GitCommand))
{
sb.AppendLine($" ```bash");
sb.AppendLine($" {op.GitCommand}");
sb.AppendLine($" ```");
}
}
sb.AppendLine();
}
}
return sb.ToString();
}
private string ExportToHtml(HistoryHealthReport report)
{
// Simple HTML wrapper around markdown content
var markdown = ExportToMarkdown(report);
return $@"<!DOCTYPE html>
<html>
<head>
<title>Git Health Report - {report.RepoName}</title>
<style>
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px; }}
h1 {{ color: #333; }}
h2 {{ color: #444; border-bottom: 1px solid #ddd; padding-bottom: 8px; }}
h3 {{ color: #555; }}
table {{ border-collapse: collapse; width: 100%; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background-color: #f5f5f5; }}
code {{ background-color: #f5f5f5; padding: 2px 6px; border-radius: 3px; }}
pre {{ background-color: #f5f5f5; padding: 12px; border-radius: 6px; overflow-x: auto; }}
</style>
</head>
<body>
<pre>{System.Web.HttpUtility.HtmlEncode(markdown)}</pre>
</body>
</html>";
}
private string ExportToConsole(HistoryHealthReport report)
{
var sb = new StringBuilder();
sb.AppendLine($"╔══════════════════════════════════════════════════════════════╗");
sb.AppendLine($"║ GIT HISTORY HEALTH REPORT ║");
sb.AppendLine($"╠══════════════════════════════════════════════════════════════╣");
sb.AppendLine($"║ Repository: {report.RepoName,-48} ║");
sb.AppendLine($"║ Branch: {report.CurrentBranch,-52} ║");
sb.AppendLine($"║ Commits: {report.CommitsAnalyzed,-51} ║");
sb.AppendLine($"╠══════════════════════════════════════════════════════════════╣");
sb.AppendLine($"║ OVERALL SCORE: {report.Score.OverallScore,3}/100 Grade: {report.Score.Grade,-22} ║");
sb.AppendLine($"╠══════════════════════════════════════════════════════════════╣");
sb.AppendLine($"║ Components: ║");
sb.AppendLine($"║ Messages: {report.Score.ComponentScores.MessageScore,3}/100 {GetBar(report.Score.ComponentScores.MessageScore),-30} ║");
sb.AppendLine($"║ Merges: {report.Score.ComponentScores.MergeScore,3}/100 {GetBar(report.Score.ComponentScores.MergeScore),-30} ║");
sb.AppendLine($"║ Duplicates: {report.Score.ComponentScores.DuplicateScore,3}/100 {GetBar(report.Score.ComponentScores.DuplicateScore),-30} ║");
sb.AppendLine($"║ Branches: {report.Score.ComponentScores.BranchScore,3}/100 {GetBar(report.Score.ComponentScores.BranchScore),-30} ║");
sb.AppendLine($"║ Authorship: {report.Score.ComponentScores.AuthorshipScore,3}/100 {GetBar(report.Score.ComponentScores.AuthorshipScore),-30} ║");
sb.AppendLine($"╠══════════════════════════════════════════════════════════════╣");
sb.AppendLine($"║ Issues: {report.CriticalIssueCount} critical, {report.ErrorCount} errors, {report.WarningCount} warnings ║");
sb.AppendLine($"╚══════════════════════════════════════════════════════════════╝");
return sb.ToString();
}
private static string GetStatusIcon(int score) => score switch
{
>= 90 => "✅ Excellent",
>= 70 => "👍 Good",
>= 50 => "⚠️ Fair",
>= 30 => "❌ Poor",
_ => "🚨 Critical"
};
private static string GetBar(int score)
{
var filled = score / 5;
var empty = 20 - filled;
return $"[{new string('█', filled)}{new string('░', empty)}]";
}
}

View File

@@ -0,0 +1,665 @@
using MarketAlly.GitCommitEditor.Models;
using MarketAlly.GitCommitEditor.Models.HistoryHealth;
using MarketAlly.GitCommitEditor.Resources;
using MarketAlly.LibGit2Sharp;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Analyzes git repository history health.
/// </summary>
public sealed class HistoryHealthAnalyzer : IHistoryHealthAnalyzer
{
private readonly ICommitAnalyzer _commitAnalyzer;
private readonly HealthScoringWeights _weights;
// Patterns that indicate merge fix commits
private static readonly string[] MergeFixPatterns =
[
"fix merge",
"merge fix",
"resolve conflict",
"fix conflict",
"meerge", // Common typo
"megre", // Common typo
"fixed merge"
];
public HistoryHealthAnalyzer(ICommitAnalyzer commitAnalyzer, HealthScoringWeights? weights = null)
{
_commitAnalyzer = commitAnalyzer;
_weights = weights ?? HealthScoringWeights.Default;
}
public Task<HistoryHealthAnalysis> AnalyzeAsync(
string repoPath,
HistoryAnalysisOptions? options = null,
IProgress<AnalysisProgress>? progress = null,
CancellationToken ct = default)
{
var repo = new ManagedRepo
{
Path = repoPath,
Name = Path.GetFileName(repoPath)
};
return AnalyzeAsync(repo, options, progress, ct);
}
public async Task<HistoryHealthAnalysis> AnalyzeAsync(
ManagedRepo managedRepo,
HistoryAnalysisOptions? options = null,
IProgress<AnalysisProgress>? progress = null,
CancellationToken ct = default)
{
options ??= new HistoryAnalysisOptions();
using var repo = new Repository(managedRepo.Path);
// Get commits to analyze
progress?.Report(new AnalysisProgress { CurrentStage = Str.Health_LoadingCommits, PercentComplete = 0 });
var commits = GetCommitsToAnalyze(repo, options).ToList();
var totalCommits = commits.Count;
progress?.Report(new AnalysisProgress
{
CurrentStage = Str.Health_LoadingCommits,
PercentComplete = 10,
TotalCommits = totalCommits
});
// Analyze duplicates
progress?.Report(new AnalysisProgress
{
CurrentStage = Str.Health_DetectingDuplicates,
PercentComplete = 20,
TotalCommits = totalCommits
});
var duplicateMetrics = options.IncludeDuplicateDetection
? await Task.Run(() => AnalyzeDuplicates(commits), ct)
: CreateEmptyDuplicateMetrics(totalCommits);
ct.ThrowIfCancellationRequested();
// Analyze merges
progress?.Report(new AnalysisProgress
{
CurrentStage = Str.Health_AnalyzingMerges,
PercentComplete = 40,
TotalCommits = totalCommits
});
var mergeMetrics = await Task.Run(() => AnalyzeMerges(commits), ct);
ct.ThrowIfCancellationRequested();
// Analyze branches
progress?.Report(new AnalysisProgress
{
CurrentStage = Str.Health_AnalyzingBranches,
PercentComplete = 50,
TotalCommits = totalCommits
});
var branchMetrics = options.IncludeBranchAnalysis
? await Task.Run(() => AnalyzeBranches(repo, commits), ct)
: CreateEmptyBranchMetrics();
ct.ThrowIfCancellationRequested();
// Analyze message quality
progress?.Report(new AnalysisProgress
{
CurrentStage = Str.Health_AnalyzingMessages,
PercentComplete = 60,
TotalCommits = totalCommits
});
var messageDistribution = options.IncludeMessageDistribution
? await Task.Run(() => AnalyzeMessageQuality(commits, progress, totalCommits), ct)
: CreateEmptyMessageDistribution(totalCommits);
ct.ThrowIfCancellationRequested();
// Analyze authorship
progress?.Report(new AnalysisProgress
{
CurrentStage = Str.Health_AnalyzingAuthorship,
PercentComplete = 90,
TotalCommits = totalCommits
});
var authorshipMetrics = await Task.Run(() => AnalyzeAuthorship(commits), ct);
progress?.Report(new AnalysisProgress
{
CurrentStage = Str.Health_Complete,
PercentComplete = 100,
TotalCommits = totalCommits,
CommitsProcessed = totalCommits
});
return new HistoryHealthAnalysis
{
RepoPath = managedRepo.Path,
RepoName = managedRepo.Name,
CurrentBranch = repo.Head.FriendlyName,
CommitsAnalyzed = totalCommits,
OldestCommitDate = commits.LastOrDefault()?.Author.When,
NewestCommitDate = commits.FirstOrDefault()?.Author.When,
Duplicates = duplicateMetrics,
MergeMetrics = mergeMetrics,
BranchMetrics = branchMetrics,
MessageDistribution = messageDistribution,
AuthorshipMetrics = authorshipMetrics
};
}
private IEnumerable<Commit> GetCommitsToAnalyze(Repository repo, HistoryAnalysisOptions options)
{
var filter = new CommitFilter
{
SortBy = CommitSortStrategies.Topological | CommitSortStrategies.Time
};
IEnumerable<Commit> commits = repo.Commits.QueryBy(filter);
if (options.AnalyzeSince.HasValue)
{
commits = commits.Where(c => c.Author.When >= options.AnalyzeSince.Value);
}
return commits.Take(options.EffectiveMaxCommits);
}
private DuplicateCommitMetrics AnalyzeDuplicates(List<Commit> commits)
{
var duplicateGroups = new List<DuplicateCommitGroup>();
var messageGroups = new Dictionary<string, List<Commit>>();
var treeGroups = new Dictionary<string, List<Commit>>();
foreach (var commit in commits)
{
// Skip merge commits for duplicate detection
if (commit.Parents.Count() > 1) continue;
var message = NormalizeMessage(commit.MessageShort);
var treeSha = commit.Tree.Sha;
// Group by normalized message
if (!messageGroups.ContainsKey(message))
messageGroups[message] = [];
messageGroups[message].Add(commit);
// Group by tree SHA
if (!treeGroups.ContainsKey(treeSha))
treeGroups[treeSha] = [];
treeGroups[treeSha].Add(commit);
}
// Find exact tree duplicates
foreach (var group in treeGroups.Where(g => g.Value.Count > 1))
{
duplicateGroups.Add(new DuplicateCommitGroup
{
CanonicalMessage = group.Value.First().MessageShort,
CommitHashes = group.Value.Select(c => c.Sha).ToList(),
Type = DuplicateType.ExactTree
});
}
// Find exact message duplicates (not already in tree duplicates)
var treeDuplicateHashes = new HashSet<string>(
duplicateGroups.SelectMany(g => g.CommitHashes));
foreach (var group in messageGroups.Where(g => g.Value.Count > 1))
{
var nonTreeDuplicates = group.Value
.Where(c => !treeDuplicateHashes.Contains(c.Sha))
.ToList();
if (nonTreeDuplicates.Count > 1)
{
duplicateGroups.Add(new DuplicateCommitGroup
{
CanonicalMessage = group.Key,
CommitHashes = nonTreeDuplicates.Select(c => c.Sha).ToList(),
Type = DuplicateType.ExactMessage
});
}
}
var exactDuplicates = duplicateGroups
.Where(g => g.Type == DuplicateType.ExactTree)
.Sum(g => g.InstanceCount - 1);
return new DuplicateCommitMetrics
{
TotalCommitsAnalyzed = commits.Count,
TotalDuplicateGroups = duplicateGroups.Count,
TotalDuplicateInstances = duplicateGroups.Sum(g => g.InstanceCount - 1),
ExactDuplicates = exactDuplicates,
CherryPicks = 0, // Would need patch-id comparison
FuzzyMatches = duplicateGroups.Count(g => g.Type == DuplicateType.FuzzyMessage),
DuplicateGroups = duplicateGroups
};
}
private MergeCommitMetrics AnalyzeMerges(List<Commit> commits)
{
var mergeCommits = commits.Where(c => c.Parents.Count() > 1).ToList();
var nonMergeCommits = commits.Where(c => c.Parents.Count() <= 1).ToList();
var mergeFixCommits = new List<string>();
var messyPatterns = new List<string>();
foreach (var commit in commits)
{
var msgLower = commit.MessageShort.ToLowerInvariant();
foreach (var pattern in MergeFixPatterns)
{
if (msgLower.Contains(pattern))
{
if (commit.Parents.Count() <= 1)
{
mergeFixCommits.Add(commit.Sha);
}
if (!messyPatterns.Contains(pattern))
{
messyPatterns.Add(pattern);
}
break;
}
}
}
// Count null merges (empty merge commits)
var nullMerges = mergeCommits.Count(m =>
{
var parents = m.Parents.ToList();
return parents.Count == 2 && m.Tree.Sha == parents[0].Tree.Sha;
});
return new MergeCommitMetrics
{
TotalCommits = commits.Count,
TotalMerges = mergeCommits.Count,
TrivialMerges = mergeCommits.Count - nullMerges,
ConflictMerges = 0, // Hard to detect reliably
MergeFixCommits = mergeFixCommits.Count,
NullMerges = nullMerges,
AverageMergeComplexity = 0, // Would need tree diff
MessyMergePatterns = messyPatterns,
MergeFixCommitHashes = mergeFixCommits
};
}
private BranchComplexityMetrics AnalyzeBranches(Repository repo, List<Commit> commits)
{
var branches = repo.Branches.Where(b => !b.IsRemote).ToList();
var remoteBranches = repo.Branches.Where(b => b.IsRemote).ToList();
var now = DateTimeOffset.UtcNow;
var staleDays = 30;
var staleBranches = new List<string>();
var activeBranches = 0;
foreach (var branch in branches)
{
if (branch.Tip == null) continue;
var age = now - branch.Tip.Author.When;
if (age.TotalDays > staleDays)
{
staleBranches.Add(branch.FriendlyName);
}
else
{
activeBranches++;
}
}
// Count merge commits to determine cross-merges
var mergeCommits = commits.Where(c => c.Parents.Count() > 1).ToList();
var mainBranchNames = new[] { "main", "master", "develop", "dev" };
// Simplified cross-merge detection
var crossMerges = 0;
foreach (var merge in mergeCommits)
{
var msg = merge.MessageShort.ToLowerInvariant();
// If merge message doesn't mention main branches, it's likely a cross-merge
if (!mainBranchNames.Any(b => msg.Contains(b)))
{
crossMerges++;
}
}
// Determine topology
var mergeRatio = commits.Count > 0 ? (double)mergeCommits.Count / commits.Count : 0;
var crossMergeRatio = mergeCommits.Count > 0 ? (double)crossMerges / mergeCommits.Count : 0;
var topology = DetermineTopology(mergeRatio, crossMergeRatio, staleBranches.Count);
return new BranchComplexityMetrics
{
TotalBranches = branches.Count, // Only count local branches
ActiveBranches = activeBranches,
StaleBranches = staleBranches.Count,
CrossMerges = crossMerges,
AverageBranchAge = 0, // Would need more calculation
AverageBranchLength = 0,
LongLivedBranches = 0,
Topology = topology,
StaleBranchNames = staleBranches
};
}
private BranchTopologyType DetermineTopology(double mergeRatio, double crossMergeRatio, int staleBranches)
{
if (mergeRatio < 0.1)
return BranchTopologyType.Linear;
if (crossMergeRatio > 0.5 || (mergeRatio > 0.5 && staleBranches > 5))
return BranchTopologyType.Spaghetti;
if (crossMergeRatio > 0.2 || mergeRatio > 0.4)
return BranchTopologyType.Tangled;
if (mergeRatio > 0.2)
return BranchTopologyType.GitFlow;
return BranchTopologyType.Balanced;
}
private MessageQualityDistribution AnalyzeMessageQuality(
List<Commit> commits,
IProgress<AnalysisProgress>? progress,
int totalCommits)
{
var scores = new List<(string Hash, int Score, DateTimeOffset Date)>();
var poorCommits = new List<string>();
var processed = 0;
foreach (var commit in commits)
{
// Skip merge commits for message quality
if (commit.Parents.Count() > 1) continue;
var analysis = _commitAnalyzer.Analyze(commit.Message);
scores.Add((commit.Sha, analysis.OverallScore, commit.Author.When));
if (analysis.OverallScore < 50)
{
poorCommits.Add(commit.Sha);
}
processed++;
if (processed % 100 == 0)
{
progress?.Report(new AnalysisProgress
{
CurrentStage = Str.Health_AnalyzingMessages,
PercentComplete = 60 + (int)(30.0 * processed / totalCommits),
CommitsProcessed = processed,
TotalCommits = totalCommits
});
}
}
if (scores.Count == 0)
{
return CreateEmptyMessageDistribution(totalCommits);
}
var sortedScores = scores.Select(s => s.Score).OrderBy(s => s).ToList();
var avg = sortedScores.Average();
var median = sortedScores[sortedScores.Count / 2];
// Calculate standard deviation
var sumSquaredDiff = sortedScores.Sum(s => Math.Pow(s - avg, 2));
var stdDev = Math.Sqrt(sumSquaredDiff / sortedScores.Count);
// Determine trend (compare first half vs second half)
var midpoint = scores.Count / 2;
var olderAvg = scores.Skip(midpoint).Average(s => s.Score);
var newerAvg = scores.Take(midpoint).Average(s => s.Score);
var trend = newerAvg > olderAvg + 5 ? TrendDirection.Improving
: newerAvg < olderAvg - 5 ? TrendDirection.Declining
: TrendDirection.Stable;
return new MessageQualityDistribution
{
TotalCommits = scores.Count,
Excellent = scores.Count(s => s.Score >= 90),
Good = scores.Count(s => s.Score >= 70 && s.Score < 90),
Fair = scores.Count(s => s.Score >= 50 && s.Score < 70),
Poor = scores.Count(s => s.Score < 50),
AverageScore = avg,
MedianScore = median,
StandardDeviation = stdDev,
Trend = trend,
Clusters = [], // Could add cluster detection
PoorCommitHashes = poorCommits
};
}
private AuthorshipMetrics AnalyzeAuthorship(List<Commit> commits)
{
// Track running totals for calculating averages
var authorData = new Dictionary<string, (string Name, string Email, int CommitCount, double TotalQuality, int MergeCount)>();
var emailToCanonical = new Dictionary<string, string>(); // Maps raw email to canonical key
var nameToCanonical = new Dictionary<string, string>(); // Maps normalized name to canonical key
var missingEmail = 0;
var invalidEmail = 0;
var botCommits = 0;
var botPatterns = new[] { "[bot]", "dependabot", "renovate", "github-actions", "noreply" };
foreach (var commit in commits)
{
var email = commit.Author.Email ?? "";
var name = commit.Author.Name ?? "Unknown";
var normalizedEmail = NormalizeEmail(email);
var normalizedName = NormalizeName(name);
if (string.IsNullOrWhiteSpace(email))
{
missingEmail++;
}
else if (!email.Contains('@'))
{
invalidEmail++;
}
var isMerge = commit.Parents.Count() > 1;
var isBot = botPatterns.Any(p =>
name.Contains(p, StringComparison.OrdinalIgnoreCase) ||
email.Contains(p, StringComparison.OrdinalIgnoreCase));
if (isBot) botCommits++;
// Analyze message quality for this commit
var quality = _commitAnalyzer.Analyze(commit.Message);
var qualityScore = quality.OverallScore;
// Find or create canonical key for this author
var canonicalKey = FindOrCreateCanonicalKey(
normalizedEmail, normalizedName, name, email,
emailToCanonical, nameToCanonical, authorData);
if (!authorData.TryGetValue(canonicalKey, out var data))
{
data = (name, email, 0, 0, 0);
}
// Update running totals
authorData[canonicalKey] = (
data.Name,
data.Email,
data.CommitCount + 1,
data.TotalQuality + qualityScore,
data.MergeCount + (isMerge ? 1 : 0)
);
}
// Convert to AuthorStats with calculated averages
var authorStats = authorData.ToDictionary(
kvp => kvp.Key,
kvp => new AuthorStats
{
Name = kvp.Value.Name,
Email = kvp.Value.Email,
CommitCount = kvp.Value.CommitCount,
AverageMessageQuality = kvp.Value.CommitCount > 0
? kvp.Value.TotalQuality / kvp.Value.CommitCount
: 0,
MergeCommitCount = kvp.Value.MergeCount
});
return new AuthorshipMetrics
{
TotalAuthors = authorStats.Count,
TotalCommits = commits.Count,
MissingEmailCount = missingEmail,
InvalidEmailCount = invalidEmail,
BotCommits = botCommits,
AuthorBreakdown = authorStats
};
}
private static string FindOrCreateCanonicalKey<T>(
string normalizedEmail,
string normalizedName,
string rawName,
string rawEmail,
Dictionary<string, string> emailToCanonical,
Dictionary<string, string> nameToCanonical,
Dictionary<string, T> authorData)
{
// First, check if we've seen this exact email before
if (!string.IsNullOrEmpty(normalizedEmail) && emailToCanonical.TryGetValue(normalizedEmail, out var existingKey))
{
return existingKey;
}
// Next, check if we've seen this name before (for matching when emails differ)
if (!string.IsNullOrEmpty(normalizedName) && nameToCanonical.TryGetValue(normalizedName, out existingKey))
{
// Also map this email to the same canonical key
if (!string.IsNullOrEmpty(normalizedEmail))
{
emailToCanonical[normalizedEmail] = existingKey;
}
return existingKey;
}
// Create new canonical key - prefer email if valid, otherwise use name
var canonicalKey = !string.IsNullOrEmpty(normalizedEmail) ? normalizedEmail : normalizedName;
// Register mappings
if (!string.IsNullOrEmpty(normalizedEmail))
{
emailToCanonical[normalizedEmail] = canonicalKey;
}
if (!string.IsNullOrEmpty(normalizedName))
{
nameToCanonical[normalizedName] = canonicalKey;
}
return canonicalKey;
}
private static string NormalizeEmail(string email)
{
if (string.IsNullOrWhiteSpace(email)) return "";
email = email.ToLowerInvariant().Trim();
// Remove + aliases (e.g., john+test@gmail.com -> john@gmail.com)
var atIndex = email.IndexOf('@');
if (atIndex > 0)
{
var plusIndex = email.IndexOf('+');
if (plusIndex > 0 && plusIndex < atIndex)
{
email = email[..plusIndex] + email[atIndex..];
}
}
// Normalize common noreply patterns
if (email.Contains("noreply") || email.Contains("no-reply"))
{
// Extract username from GitHub noreply format: 12345678+username@users.noreply.github.com
var match = System.Text.RegularExpressions.Regex.Match(email, @"\d+\+([^@]+)@users\.noreply\.github\.com");
if (match.Success)
{
return match.Groups[1].Value + "@github";
}
}
return email;
}
private static string NormalizeName(string name)
{
if (string.IsNullOrWhiteSpace(name)) return "";
// Lowercase and trim
name = name.ToLowerInvariant().Trim();
// Remove common suffixes/prefixes
name = name.Replace("[bot]", "").Trim();
// Normalize whitespace
name = System.Text.RegularExpressions.Regex.Replace(name, @"\s+", " ");
return name;
}
private static string NormalizeMessage(string message)
{
return message
.ToLowerInvariant()
.Trim()
.Replace("\r", "")
.Replace("\n", " ");
}
private static DuplicateCommitMetrics CreateEmptyDuplicateMetrics(int totalCommits) => new()
{
TotalCommitsAnalyzed = totalCommits,
TotalDuplicateGroups = 0,
TotalDuplicateInstances = 0,
ExactDuplicates = 0,
CherryPicks = 0,
FuzzyMatches = 0,
DuplicateGroups = []
};
private static BranchComplexityMetrics CreateEmptyBranchMetrics() => new()
{
TotalBranches = 0,
ActiveBranches = 0,
StaleBranches = 0,
CrossMerges = 0,
AverageBranchAge = 0,
AverageBranchLength = 0,
LongLivedBranches = 0,
Topology = BranchTopologyType.Linear,
StaleBranchNames = []
};
private static MessageQualityDistribution CreateEmptyMessageDistribution(int totalCommits) => new()
{
TotalCommits = totalCommits,
Excellent = 0,
Good = 0,
Fair = 0,
Poor = 0,
AverageScore = 0,
MedianScore = 0,
StandardDeviation = 0,
Trend = TrendDirection.Stable,
Clusters = [],
PoorCommitHashes = []
};
}

36
Services/IApiKeyProvider.cs Executable file
View File

@@ -0,0 +1,36 @@
using MarketAlly.AIPlugin;
using MarketAlly.AIPlugin.Conversation;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Provides access to API keys for AI providers.
/// Implement this interface in platform-specific code (e.g., using SecureStorage in MAUI).
/// </summary>
public interface IApiKeyProvider
{
/// <summary>
/// Gets the API key for the specified provider, or null if not configured.
/// </summary>
string? GetApiKey(AIProvider provider);
/// <summary>
/// Checks if an API key is configured for the specified provider.
/// </summary>
bool HasApiKey(AIProvider provider);
/// <summary>
/// Sets the API key for the specified provider.
/// </summary>
Task SetApiKeyAsync(AIProvider provider, string apiKey);
/// <summary>
/// Removes the API key for the specified provider.
/// </summary>
Task RemoveApiKeyAsync(AIProvider provider);
/// <summary>
/// Gets all providers that have API keys configured.
/// </summary>
IReadOnlyList<AIProvider> GetConfiguredProviders();
}

View File

@@ -0,0 +1,124 @@
using MarketAlly.GitCommitEditor.Models;
using MarketAlly.GitCommitEditor.Models.HistoryHealth;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Executes cleanup operations on git repositories.
/// </summary>
public interface ICleanupExecutor
{
/// <summary>
/// Executes a single cleanup operation.
/// </summary>
Task<CleanupExecutionResult> ExecuteAsync(
ManagedRepo repo,
CleanupOperation operation,
CleanupExecutionOptions? options = null,
IProgress<CleanupProgress>? progress = null,
CancellationToken ct = default);
/// <summary>
/// Executes multiple cleanup operations in sequence.
/// </summary>
Task<BatchCleanupResult> ExecuteBatchAsync(
ManagedRepo repo,
IEnumerable<CleanupOperation> operations,
CleanupExecutionOptions? options = null,
IProgress<CleanupProgress>? progress = null,
CancellationToken ct = default);
/// <summary>
/// Previews what a cleanup operation will do without executing it.
/// </summary>
Task<CleanupPreview> PreviewAsync(
ManagedRepo repo,
CleanupOperation operation,
CancellationToken ct = default);
/// <summary>
/// Creates a backup branch before cleanup operations.
/// </summary>
Task<string> CreateBackupBranchAsync(
ManagedRepo repo,
string? branchName = null,
CancellationToken ct = default);
}
/// <summary>
/// Options for cleanup execution.
/// </summary>
public sealed record CleanupExecutionOptions
{
/// <summary>
/// Create a backup branch before making changes.
/// </summary>
public bool CreateBackup { get; init; } = true;
/// <summary>
/// Custom backup branch name. Auto-generated if null.
/// </summary>
public string? BackupBranchName { get; init; }
/// <summary>
/// Allow operations on pushed commits (requires force push).
/// </summary>
public bool AllowPushedCommits { get; init; } = false;
/// <summary>
/// Automatically force push after rewriting history.
/// </summary>
public bool AutoForcePush { get; init; } = false;
/// <summary>
/// Use AI to generate improved commit messages.
/// </summary>
public bool UseAiForMessages { get; init; } = true;
}
/// <summary>
/// Result of executing a single cleanup operation.
/// </summary>
public sealed record CleanupExecutionResult
{
public required string OperationId { get; init; }
public required CleanupType Type { get; init; }
public bool Success { get; init; }
public string? ErrorMessage { get; init; }
public int CommitsModified { get; init; }
public int CommitsRemoved { get; init; }
public string? BackupBranch { get; init; }
public bool RequiresForcePush { get; init; }
public IReadOnlyList<string> ModifiedCommitHashes { get; init; } = [];
public IReadOnlyList<string> NewCommitHashes { get; init; } = [];
public TimeSpan Duration { get; init; }
}
/// <summary>
/// Result of executing multiple cleanup operations.
/// </summary>
public sealed class BatchCleanupResult
{
public int TotalOperations { get; init; }
public int Successful { get; init; }
public int Failed { get; init; }
public int Skipped { get; init; }
public string? BackupBranch { get; init; }
public bool RequiresForcePush { get; init; }
public IReadOnlyList<CleanupExecutionResult> Results { get; init; } = [];
public TimeSpan TotalDuration { get; init; }
public bool AllSucceeded => Failed == 0 && Skipped == 0;
}
/// <summary>
/// Progress information for cleanup operations.
/// </summary>
public sealed class CleanupProgress
{
public required string CurrentOperation { get; init; }
public int CurrentIndex { get; init; }
public int TotalOperations { get; init; }
public int PercentComplete { get; init; }
public string? CurrentCommit { get; init; }
}

View File

@@ -0,0 +1,18 @@
using MarketAlly.GitCommitEditor.Models;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Provides commit analysis functionality.
/// </summary>
public interface ICommitAnalysisService
{
Task<IReadOnlyList<CommitAnalysis>> AnalyzeAllReposAsync(
bool onlyNeedsImprovement = true,
IProgress<(string Repo, int Processed)>? progress = null,
CancellationToken ct = default);
IEnumerable<CommitAnalysis> AnalyzeRepo(ManagedRepo repo);
CommitAnalysis AnalyzeCommit(string repoPath, string commitHash);
Task UpdateRepoAnalysisAsync(ManagedRepo repo, int totalCommits, int commitsNeedingImprovement, CancellationToken ct = default);
}

View File

@@ -0,0 +1,33 @@
using MarketAlly.GitCommitEditor.Models;
using MarketAlly.GitCommitEditor.Options;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Simple commit message analyzer for health analysis.
/// </summary>
public interface ICommitAnalyzer
{
/// <summary>
/// Analyzes a commit message and returns quality information.
/// </summary>
MessageQualityScore Analyze(string message);
}
/// <summary>
/// Default implementation using the existing analyzer.
/// </summary>
public sealed class CommitAnalyzer : ICommitAnalyzer
{
private readonly ICommitMessageAnalyzer _analyzer;
public CommitAnalyzer(CommitMessageRules? rules = null)
{
_analyzer = new CommitMessageAnalyzer(rules ?? new CommitMessageRules());
}
public MessageQualityScore Analyze(string message)
{
return _analyzer.Analyze(message);
}
}

View File

@@ -0,0 +1,14 @@
using MarketAlly.GitCommitEditor.Models;
namespace MarketAlly.GitCommitEditor.Services;
public interface ICommitMessageAnalyzer
{
MessageQualityScore Analyze(string message);
/// <summary>
/// Analyzes a commit message with context about the actual changes.
/// This allows detection of vague messages that don't describe significant changes.
/// </summary>
MessageQualityScore Analyze(string message, CommitContext context);
}

View File

@@ -0,0 +1,48 @@
using MarketAlly.GitCommitEditor.Models;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Handles commit message rewriting operations.
/// </summary>
public interface ICommitRewriteService
{
IReadOnlyList<RewriteOperation> History { get; }
IReadOnlyList<RewriteOperation> PreviewChanges(IEnumerable<CommitAnalysis> analyses);
Task<BatchResult> ApplyChangesAsync(
IEnumerable<RewriteOperation> operations,
bool dryRun = true,
IProgress<(int Processed, int Total)>? progress = null,
CancellationToken ct = default);
Task<RewriteOperation> ApplyChangeAsync(CommitAnalysis analysis, CancellationToken ct = default);
bool UndoCommitAmend(string repoPath, string originalCommitHash);
/// <summary>
/// Gets safety information for a batch rewrite operation.
/// Checks for uncommitted changes, pushed commits, and remote tracking status.
/// </summary>
/// <param name="repoPath">The repository path.</param>
/// <param name="commits">The commits to be rewritten.</param>
/// <returns>Safety information about the proposed operation.</returns>
RewriteSafetyInfo GetRewriteSafetyInfo(string repoPath, IEnumerable<CommitAnalysis> commits);
/// <summary>
/// Executes a batch rewrite operation with full safety checks and backup creation.
/// </summary>
/// <param name="repoPath">The repository path.</param>
/// <param name="commits">The commits to rewrite (must have SuggestedMessage populated).</param>
/// <param name="createBackup">Whether to create a backup branch before rewriting.</param>
/// <param name="progress">Progress reporter.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Result of the batch rewrite operation.</returns>
Task<BatchRewriteResult> ExecuteBatchRewriteAsync(
string repoPath,
IEnumerable<CommitAnalysis> commits,
bool createBackup = true,
IProgress<(int Current, int Total, string CommitHash)>? progress = null,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,110 @@
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Tracks AI operation costs for the session
/// </summary>
public interface ICostTrackingService
{
/// <summary>
/// Total cost for the current session
/// </summary>
decimal SessionCost { get; }
/// <summary>
/// Total cost across all sessions (lifetime)
/// </summary>
decimal LifetimeCost { get; }
/// <summary>
/// Total input tokens used this session
/// </summary>
int TotalInputTokens { get; }
/// <summary>
/// Total output tokens used this session
/// </summary>
int TotalOutputTokens { get; }
/// <summary>
/// Number of AI operations performed this session
/// </summary>
int OperationCount { get; }
/// <summary>
/// Number of AI operations performed across all sessions (lifetime)
/// </summary>
int LifetimeOperationCount { get; }
/// <summary>
/// Records an AI operation's cost
/// </summary>
/// <param name="operationType">Type of operation (e.g., "CommitSuggestion", "GitDiagnosis")</param>
/// <param name="inputTokens">Input tokens used</param>
/// <param name="outputTokens">Output tokens used</param>
/// <param name="cost">Calculated cost</param>
void RecordOperation(string operationType, int inputTokens, int outputTokens, decimal cost);
/// <summary>
/// Gets the cost breakdown by operation type
/// </summary>
IReadOnlyDictionary<string, OperationCostSummary> GetCostBreakdown();
/// <summary>
/// Resets session tracking (keeps lifetime totals)
/// </summary>
void ResetSession();
/// <summary>
/// Resets all tracking including lifetime totals
/// </summary>
void ResetAll();
/// <summary>
/// Saves the current state to persistent storage
/// </summary>
void SaveState();
/// <summary>
/// Loads state from persistent storage
/// </summary>
void LoadState();
/// <summary>
/// Event raised when costs are updated
/// </summary>
event EventHandler<CostUpdatedEventArgs>? CostUpdated;
}
/// <summary>
/// Summary of costs for a specific operation type
/// </summary>
public class OperationCostSummary
{
public string OperationType { get; set; } = string.Empty;
public int Count { get; set; }
public int TotalInputTokens { get; set; }
public int TotalOutputTokens { get; set; }
public decimal TotalCost { get; set; }
public decimal AverageCost => Count > 0 ? TotalCost / Count : 0;
}
/// <summary>
/// Event args for cost updates
/// </summary>
public class CostUpdatedEventArgs : EventArgs
{
public string OperationType { get; }
public decimal OperationCost { get; }
public decimal SessionTotal { get; }
public int InputTokens { get; }
public int OutputTokens { get; }
public CostUpdatedEventArgs(string operationType, decimal operationCost, decimal sessionTotal, int inputTokens, int outputTokens)
{
OperationType = operationType;
OperationCost = operationCost;
SessionTotal = sessionTotal;
InputTokens = inputTokens;
OutputTokens = outputTokens;
}
}

View File

@@ -0,0 +1,25 @@
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Unified facade for all git commit message improvement operations.
/// Composes smaller, focused interfaces for consumers who need full functionality.
/// </summary>
public interface IGitMessageImproverService :
IRepositoryManager,
ICommitAnalysisService,
ISuggestionService,
ICommitRewriteService,
IGitPushService,
IHistoryHealthService,
IDisposable
{
/// <summary>
/// Load saved state from disk (repos, history, etc.)
/// </summary>
Task LoadStateAsync(CancellationToken ct = default);
/// <summary>
/// Generates a summary report of all repositories and recent operations.
/// </summary>
string GenerateSummaryReport();
}

View File

@@ -0,0 +1,79 @@
using MarketAlly.GitCommitEditor.Models;
using MarketAlly.LibGit2Sharp;
namespace MarketAlly.GitCommitEditor.Services;
public interface IGitOperationsService : IDisposable
{
IEnumerable<string> DiscoverRepositories(string rootPath, int maxDepth = 3);
ManagedRepo CreateManagedRepo(string repoPath);
IEnumerable<BranchInfo> GetBranches(string repoPath);
IEnumerable<CommitAnalysis> AnalyzeCommits(
ManagedRepo managedRepo,
ICommitMessageAnalyzer analyzer,
int maxCommits = 100,
DateTimeOffset? since = null,
string[]? excludeAuthors = null);
RewriteOperation AmendLatestCommit(ManagedRepo managedRepo, string newMessage);
RewriteOperation RewordOlderCommit(ManagedRepo managedRepo, string commitHash, string newMessage);
/// <summary>
/// Rewords multiple commits in a single pass through history.
/// This is more efficient and reliable than multiple individual RewordOlderCommit calls,
/// because it handles the hash changes that occur when rewriting commits.
/// </summary>
/// <param name="managedRepo">The repository.</param>
/// <param name="rewrites">Dictionary mapping original commit hash to new message.</param>
/// <returns>List of rewrite operations with results.</returns>
List<RewriteOperation> RewordMultipleCommits(ManagedRepo managedRepo, Dictionary<string, string> rewrites);
/// <summary>
/// Undoes a commit amend by resetting to the original commit hash from the reflog.
/// </summary>
bool UndoCommitAmend(string repoPath, string originalCommitHash);
/// <summary>
/// Checks if a commit has been pushed to the remote tracking branch.
/// </summary>
bool IsCommitPushed(string repoPath, string commitHash);
/// <summary>
/// Gets tracking information for the current branch.
/// </summary>
TrackingInfo GetTrackingInfo(string repoPath);
/// <summary>
/// Force pushes the current branch to the remote.
/// </summary>
GitPushResult ForcePush(string repoPath, PushOptions? options = null);
/// <summary>
/// Regular push (non-force) to the remote.
/// </summary>
GitPushResult Push(string repoPath, PushOptions? options = null);
/// <summary>
/// Gets all backup branches (matching backup/* pattern).
/// </summary>
IEnumerable<BackupBranchInfo> GetBackupBranches(string repoPath);
/// <summary>
/// Deletes a local branch.
/// </summary>
bool DeleteBranch(string repoPath, string branchName);
/// <summary>
/// Invalidates the cached Repository for the given path.
/// Call after operations that modify git history to ensure fresh state on next access.
/// </summary>
void InvalidateCache(string path);
}
/// <summary>
/// Information about a backup branch.
/// </summary>
public record BackupBranchInfo(
string Name,
string FullName,
DateTimeOffset? CreatedAt,
string? LastCommitSha);

View File

@@ -0,0 +1,14 @@
using MarketAlly.GitCommitEditor.Models;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Handles git push operations and remote tracking.
/// </summary>
public interface IGitPushService
{
bool IsCommitPushed(string repoPath, string commitHash);
TrackingInfo GetTrackingInfo(string repoPath);
GitPushResult Push(string repoPath);
GitPushResult ForcePush(string repoPath);
}

View File

@@ -0,0 +1,31 @@
using MarketAlly.GitCommitEditor.Models.HistoryHealth;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Service for generating health reports from analysis results.
/// </summary>
public interface IHealthReportGenerator
{
/// <summary>
/// Generates a comprehensive health report from analysis results.
/// </summary>
HistoryHealthReport GenerateReport(HistoryHealthAnalysis analysis);
/// <summary>
/// Exports a report to the specified format.
/// </summary>
Task<string> ExportReportAsync(
HistoryHealthReport report,
ReportFormat format,
CancellationToken ct = default);
/// <summary>
/// Exports a report to a file.
/// </summary>
Task ExportReportToFileAsync(
HistoryHealthReport report,
ReportFormat format,
string outputPath,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,33 @@
using MarketAlly.GitCommitEditor.Models;
using MarketAlly.GitCommitEditor.Models.HistoryHealth;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Service for analyzing git repository history health.
/// </summary>
public interface IHistoryHealthAnalyzer
{
/// <summary>
/// Performs a comprehensive health analysis of a repository.
/// </summary>
/// <param name="repoPath">Path to the repository.</param>
/// <param name="options">Analysis options.</param>
/// <param name="progress">Progress reporter.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Analysis results.</returns>
Task<HistoryHealthAnalysis> AnalyzeAsync(
string repoPath,
HistoryAnalysisOptions? options = null,
IProgress<AnalysisProgress>? progress = null,
CancellationToken ct = default);
/// <summary>
/// Performs a comprehensive health analysis of a managed repository.
/// </summary>
Task<HistoryHealthAnalysis> AnalyzeAsync(
ManagedRepo repo,
HistoryAnalysisOptions? options = null,
IProgress<AnalysisProgress>? progress = null,
CancellationToken ct = default);
}

View File

@@ -0,0 +1,78 @@
using MarketAlly.GitCommitEditor.Models;
using MarketAlly.GitCommitEditor.Models.HistoryHealth;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Service for analyzing and reporting on git repository history health.
/// </summary>
public interface IHistoryHealthService
{
/// <summary>
/// Analyzes repository history health and generates a comprehensive report.
/// </summary>
/// <param name="repoPath">Path to the repository.</param>
/// <param name="options">Analysis options.</param>
/// <param name="progress">Progress reporter.</param>
/// <param name="ct">Cancellation token.</param>
/// <returns>Complete health report with scores, issues, and recommendations.</returns>
Task<HistoryHealthReport> AnalyzeHistoryHealthAsync(
string repoPath,
HistoryAnalysisOptions? options = null,
IProgress<AnalysisProgress>? progress = null,
CancellationToken ct = default);
/// <summary>
/// Analyzes history health for a managed repository.
/// </summary>
Task<HistoryHealthReport> AnalyzeHistoryHealthAsync(
ManagedRepo repo,
HistoryAnalysisOptions? options = null,
IProgress<AnalysisProgress>? progress = null,
CancellationToken ct = default);
/// <summary>
/// Exports a health report to the specified format.
/// </summary>
Task<string> ExportHealthReportAsync(
HistoryHealthReport report,
ReportFormat format,
CancellationToken ct = default);
/// <summary>
/// Exports a health report to a file.
/// </summary>
Task ExportHealthReportToFileAsync(
HistoryHealthReport report,
ReportFormat format,
string outputPath,
CancellationToken ct = default);
/// <summary>
/// Executes a single cleanup operation.
/// </summary>
Task<CleanupExecutionResult> ExecuteCleanupAsync(
ManagedRepo repo,
CleanupOperation operation,
CleanupExecutionOptions? options = null,
IProgress<CleanupProgress>? progress = null,
CancellationToken ct = default);
/// <summary>
/// Executes all cleanup operations from a report.
/// </summary>
Task<BatchCleanupResult> ExecuteAllCleanupsAsync(
ManagedRepo repo,
CleanupSuggestions suggestions,
CleanupExecutionOptions? options = null,
IProgress<CleanupProgress>? progress = null,
CancellationToken ct = default);
/// <summary>
/// Creates a backup branch before cleanup operations.
/// </summary>
Task<string> CreateBackupBranchAsync(
ManagedRepo repo,
string? branchName = null,
CancellationToken ct = default);
}

157
Services/IModelProviderService.cs Executable file
View File

@@ -0,0 +1,157 @@
using MarketAlly.AIPlugin;
using MarketAlly.AIPlugin.Agentic;
using MarketAlly.AIPlugin.Agentic.Models;
using MarketAlly.AIPlugin.Conversation;
using MarketAlly.GitCommitEditor.Options;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Service for managing AI model providers dynamically based on configured API keys.
/// Only shows providers that the user has actually configured.
/// </summary>
public interface IModelProviderService
{
/// <summary>
/// Gets the providers that have API keys configured.
/// </summary>
IReadOnlyList<string> AvailableProviders { get; }
/// <summary>
/// Gets models for a provider that support function calling.
/// </summary>
IReadOnlyList<string> GetModelsForProvider(string provider);
/// <summary>
/// Gets models with display information for a provider.
/// </summary>
IReadOnlyList<ModelDisplayInfo> GetModelsWithInfoForProvider(string provider);
/// <summary>
/// Gets model info by ID.
/// </summary>
ModelDisplayInfo? GetModelInfo(string modelId);
/// <summary>
/// Gets the API key for a provider.
/// </summary>
string? GetApiKey(AIProvider provider);
/// <summary>
/// Checks if a provider has an API key configured.
/// </summary>
bool HasApiKey(AIProvider provider);
/// <summary>
/// Refreshes the available providers based on current API key configuration.
/// Call this after adding or removing an API key.
/// </summary>
void RefreshProviders();
}
/// <summary>
/// Implementation of IModelProviderService that uses IApiKeyProvider to determine available providers.
/// Uses the static ModelLibrary.Default for efficient querying.
/// </summary>
public class ModelProviderService : IModelProviderService
{
private readonly IApiKeyProvider _apiKeyProvider;
public ModelProviderService(IApiKeyProvider apiKeyProvider)
{
_apiKeyProvider = apiKeyProvider;
}
public IReadOnlyList<string> AvailableProviders =>
ModelLibrary.Default.GetModelsWithFunctionCalling()
.Where(m => m.Type != ModelType.Legacy)
.Select(m => m.Provider.ToString())
.Distinct()
.OrderBy(p => p)
.ToList();
public void RefreshProviders()
{
// No-op - static library doesn't need refreshing
// API key changes are handled separately by IApiKeyProvider
}
public IReadOnlyList<string> GetModelsForProvider(string provider)
{
var aiProvider = ParseProvider(provider);
var models = ModelLibrary.Default.GetModelsByProvider(aiProvider)
.Where(m => m.SupportsFunctionCalling && m.Type != ModelType.Legacy)
.OrderByDescending(m => m.Tier)
.ThenBy(m => m.DisplayName)
.Select(m => m.ModelId)
.ToList();
return models.Count > 0 ? models : new[] { ModelConstants.Claude.Sonnet4 };
}
public IReadOnlyList<ModelDisplayInfo> GetModelsWithInfoForProvider(string provider)
{
var aiProvider = ParseProvider(provider);
var models = ModelLibrary.Default.GetModelsByProvider(aiProvider)
.Where(m => m.SupportsFunctionCalling && m.Type != ModelType.Legacy)
.OrderByDescending(m => m.Tier)
.ThenBy(m => m.DisplayName)
.Select(m => new ModelDisplayInfo
{
ModelId = m.ModelId,
DisplayName = m.DisplayName,
Tier = m.Tier.ToString(),
InputCostPer1MTokens = m.InputCostPer1MTokens,
OutputCostPer1MTokens = m.OutputCostPer1MTokens,
Speed = m.Speed.ToString()
})
.ToList();
return models.Count > 0 ? models : GetDefaultModelInfo();
}
public ModelDisplayInfo? GetModelInfo(string modelId)
{
var model = ModelLibrary.Default.GetModel(modelId);
if (model == null) return null;
return new ModelDisplayInfo
{
ModelId = model.ModelId,
DisplayName = model.DisplayName,
Tier = model.Tier.ToString(),
InputCostPer1MTokens = model.InputCostPer1MTokens,
OutputCostPer1MTokens = model.OutputCostPer1MTokens,
Speed = model.Speed.ToString()
};
}
public string? GetApiKey(AIProvider provider) => _apiKeyProvider.GetApiKey(provider);
public bool HasApiKey(AIProvider provider) => _apiKeyProvider.HasApiKey(provider);
private static IReadOnlyList<ModelDisplayInfo> GetDefaultModelInfo() => new[]
{
new ModelDisplayInfo
{
ModelId = ModelConstants.Claude.Sonnet4,
DisplayName = "Claude Sonnet 4",
Tier = "Balanced",
InputCostPer1MTokens = 3.00m,
OutputCostPer1MTokens = 15.00m,
Speed = "Fast"
}
};
private static AIProvider ParseProvider(string? provider)
{
return provider?.ToLowerInvariant() switch
{
"claude" or "anthropic" => AIProvider.Claude,
"openai" or "gpt" => AIProvider.OpenAI,
"gemini" or "google" => AIProvider.Gemini,
"qwen" or "alibaba" => AIProvider.Qwen,
_ => AIProvider.Claude
};
}
}

View File

@@ -0,0 +1,39 @@
using MarketAlly.GitCommitEditor.Models;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Manages repository discovery and registration.
/// </summary>
public interface IRepositoryManager
{
IReadOnlyList<ManagedRepo> Repos { get; }
Task<IReadOnlyList<ManagedRepo>> ScanAndRegisterReposAsync(CancellationToken ct = default);
Task<ManagedRepo> RegisterRepoAsync(string repoPath);
Task<bool> UnregisterRepoAsync(string repoIdOrPath);
IEnumerable<BranchInfo> GetBranches(string repoPath);
/// <summary>
/// Checkout a branch in the specified repository.
/// </summary>
/// <param name="repo">The repository to switch branches in.</param>
/// <param name="branchName">The name of the branch to checkout.</param>
/// <returns>True if checkout succeeded, false otherwise.</returns>
Task<bool> CheckoutBranchAsync(ManagedRepo repo, string branchName);
/// <summary>
/// Gets all backup branches in the specified repository.
/// </summary>
IEnumerable<BackupBranchInfo> GetBackupBranches(string repoPath);
/// <summary>
/// Deletes a local branch.
/// </summary>
bool DeleteBranch(string repoPath, string branchName);
/// <summary>
/// Deletes all backup branches in the specified repository.
/// </summary>
int DeleteAllBackupBranches(string repoPath);
}

View File

@@ -0,0 +1,9 @@
using MarketAlly.GitCommitEditor.Models;
namespace MarketAlly.GitCommitEditor.Services;
public interface IStateRepository
{
Task<ImproverState> LoadAsync(CancellationToken ct = default);
Task SaveAsync(ImproverState state, CancellationToken ct = default);
}

View File

@@ -0,0 +1,24 @@
using MarketAlly.GitCommitEditor.Models;
namespace MarketAlly.GitCommitEditor.Services;
/// <summary>
/// Provides AI-powered suggestion generation for commit messages.
/// </summary>
public interface ISuggestionService
{
/// <summary>
/// Generate AI suggestions for a batch of commits.
/// Returns detailed success/failure information for each commit.
/// </summary>
Task<BatchSuggestionResult> GenerateSuggestionsAsync(
IEnumerable<CommitAnalysis> analyses,
IProgress<int>? progress = null,
CancellationToken ct = default);
/// <summary>
/// Generate an AI suggestion for a single commit.
/// Returns detailed result including success/failure and error information.
/// </summary>
Task<SuggestionResult> GenerateSuggestionAsync(CommitAnalysis analysis, CancellationToken ct = default);
}

BIN
icon.png Executable file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB