Initial commit - MarketAlly.GitCommitEditor library
This commit is contained in:
10
Models/BatchResult.cs
Normal file
10
Models/BatchResult.cs
Normal 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; } = [];
|
||||
}
|
||||
53
Models/BatchSuggestionResult.cs
Normal file
53
Models/BatchSuggestionResult.cs
Normal 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
39
Models/BranchInfo.cs
Normal 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
23
Models/CommitAnalysis.cs
Normal 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
17
Models/CommitContext.cs
Normal 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
26
Models/Enums.cs
Normal 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
|
||||
}
|
||||
207
Models/HistoryHealth/HealthEnums.cs
Normal file
207
Models/HistoryHealth/HealthEnums.cs
Normal 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
|
||||
};
|
||||
}
|
||||
144
Models/HistoryHealth/HealthMetrics.cs
Normal file
144
Models/HistoryHealth/HealthMetrics.cs
Normal 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; }
|
||||
}
|
||||
148
Models/HistoryHealth/HealthReport.cs
Normal file
148
Models/HistoryHealth/HealthReport.cs
Normal 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;
|
||||
}
|
||||
106
Models/HistoryHealth/HistoryAnalysisOptions.cs
Normal file
106
Models/HistoryHealth/HistoryAnalysisOptions.cs
Normal 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
51
Models/ImproverState.cs
Normal 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
17
Models/ManagedRepo.cs
Normal 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;
|
||||
}
|
||||
8
Models/MessageQualityScore.cs
Normal file
8
Models/MessageQualityScore.cs
Normal 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
8
Models/QualityIssue.cs
Normal 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
57
Models/Results.cs
Normal 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);
|
||||
}
|
||||
17
Models/RewriteOperation.cs
Normal file
17
Models/RewriteOperation.cs
Normal 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
171
Models/RewriteSafetyInfo.cs
Normal 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
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user