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

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
};
}