-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMigrationDiffService.cs
More file actions
314 lines (272 loc) · 11.8 KB
/
Copy pathMigrationDiffService.cs
File metadata and controls
314 lines (272 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
#nullable enable
using System.Text.RegularExpressions;
using EfMigrationDiff.Models;
using EfMigrationDiff.Repositories;
using EfMigrationDiff.Configuration;
using Microsoft.Extensions.Logging;
namespace EfMigrationDiff.Services;
/// <summary>
/// Service for comparing migrations between branches and generating diff reports.
/// </summary>
public class MigrationDiffService
{
private readonly MigrationRepository _migrationRepository;
private readonly ConflictDetectionService _conflictDetectionService;
private readonly SchemaChangeDetectorService _schemaChangeDetectorService;
private readonly ILogger<MigrationDiffService> _logger;
private readonly EfMigrationDiffOptions? _options;
/// <summary>
/// Initializes a new instance of <see cref="MigrationDiffService"/>.
/// </summary>
/// <param name="migrationRepository">Repository used to retrieve migration metadata.</param>
/// <param name="conflictDetectionService">Service that detects schema conflicts between branches.</param>
/// <param name="schemaChangeDetectorService">Service that extracts schema changes from a migration.</param>
/// <param name="logger">Logger instance for diagnostic output.</param>
/// <param name="options">
/// Optional configuration options. May contain settings such as ignored migration patterns.
/// </param>
public MigrationDiffService(
MigrationRepository migrationRepository,
ConflictDetectionService conflictDetectionService,
SchemaChangeDetectorService schemaChangeDetectorService,
ILogger<MigrationDiffService> logger,
EfMigrationDiffOptions? options = null)
{
_migrationRepository = migrationRepository;
_conflictDetectionService = conflictDetectionService;
_schemaChangeDetectorService = schemaChangeDetectorService;
_logger = logger;
_options = options;
}
/// <summary>
/// Compares migrations between two branches and generates a diff report
/// containing source‑only, target‑only, and common migrations along with
/// detected schema conflicts. Retrieves all migrations from both branches,
/// categorizes them, detects schema changes, and identifies conflicts.
/// </summary>
/// <param name="sourceBranch">The source (feature) branch to compare.</param>
/// <param name="targetBranch">The target (base) branch to compare against.</param>
/// <returns>
/// A <see cref="MigrationDiff"/> containing categorized migrations, schema changes,
/// and any detected conflicts between the two branches.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Thrown when <paramref name="sourceBranch"/> or <paramref name="targetBranch"/> is null.
/// </exception>
public MigrationDiff CompareBranches(BranchInfo sourceBranch, BranchInfo targetBranch)
{
ArgumentNullException.ThrowIfNull(sourceBranch);
ArgumentNullException.ThrowIfNull(targetBranch);
_logger.LogInformation("Starting comparison between branch {SourceBranch} and {TargetBranch}", sourceBranch.Id, targetBranch.Id);
var diff = new MigrationDiff(sourceBranch.Id, targetBranch.Id);
// Get all migrations for each branch
var sourceMigrations = GetBranchMigrations(sourceBranch);
var targetMigrations = GetBranchMigrations(targetBranch);
// Categorize migrations
CategorizeMigrations(sourceMigrations, targetMigrations, diff);
// Detect schema changes in source
foreach (var migration in sourceMigrations)
{
var changes = _schemaChangeDetectorService.DetectChanges(migration);
diff.SourceSchemaChanges.AddRange(changes);
}
// Detect schema changes in target
foreach (var migration in targetMigrations)
{
var changes = _schemaChangeDetectorService.DetectChanges(migration);
diff.TargetSchemaChanges.AddRange(changes);
}
// Detect conflicts
var conflicts = _conflictDetectionService.DetectConflicts(diff.SourceSchemaChanges, diff.TargetSchemaChanges);
foreach (var conflict in conflicts)
{
diff.AddConflict(conflict);
}
if (diff.HasConflicts())
{
_logger.LogWarning("Detected {ConflictCount} conflicts between {SourceBranch} and {TargetBranch}", diff.Conflicts.Count, sourceBranch.Id, targetBranch.Id);
}
else
{
_logger.LogInformation("No conflicts detected between {SourceBranch} and {TargetBranch}", sourceBranch.Id, targetBranch.Id);
}
diff.GenerateSummary();
return diff;
}
/// <summary>
/// Compares migrations within a single <see cref="DbContext"/> across branches.
/// Useful when a project contains multiple <see cref="DbContext"/> classes and you need
/// to isolate migration analysis to a specific database context.
/// </summary>
/// <param name="sourceBranch">The base branch to compare from.</param>
/// <param name="targetBranch">The feature or target branch to compare against.</param>
/// <param name="dbContextName">
/// The fully qualified or simple name of the <see cref="DbContext"/> class to filter migrations by.
/// Only migrations belonging to this context are included in the comparison.
/// </param>
/// <returns>
/// A <see cref="MigrationDiff"/> scoped to migrations from the specified <see cref="DbContext"/> only.
/// </returns>
public MigrationDiff CompareDbContextMigrations(
BranchInfo sourceBranch,
BranchInfo targetBranch,
string dbContextName)
{
var diff = new MigrationDiff(sourceBranch.Id, targetBranch.Id);
var sourceMigrations = GetContextMigrations(sourceBranch, dbContextName);
var targetMigrations = GetContextMigrations(targetBranch, dbContextName);
CategorizeMigrations(sourceMigrations, targetMigrations, diff);
foreach (var migration in sourceMigrations)
{
diff.SourceSchemaChanges.AddRange(_schemaChangeDetectorService.DetectChanges(migration));
}
foreach (var migration in targetMigrations)
{
diff.TargetSchemaChanges.AddRange(_schemaChangeDetectorService.DetectChanges(migration));
}
var conflicts = _conflictDetectionService.DetectConflicts(diff.SourceSchemaChanges, diff.TargetSchemaChanges);
foreach (var conflict in conflicts)
{
diff.AddConflict(conflict);
}
diff.GenerateSummary();
return diff;
}
/// <summary>
/// Gets all migrations for a specific branch, respecting the ignore list.
/// </summary>
private List<Migration> GetBranchMigrations(BranchInfo branch)
{
var migrations = new List<Migration>();
foreach (var migrationId in branch.MigrationIds)
{
var migration = _migrationRepository.GetById(migrationId);
if (migration is not null)
{
var identifier = migration.Name ?? migration.Id;
if (IsIgnored(identifier))
{
_logger.LogInformation("Skipping ignored migration {Migration}", identifier);
continue;
}
migrations.Add(migration);
}
}
return migrations.OrderBy(m => m.Sequence).ToList();
}
/// <summary>
/// Gets migrations for a specific <see cref="DbContext"/> in a branch, respecting the ignore list.
/// </summary>
private List<Migration> GetContextMigrations(BranchInfo branch, string dbContextName)
{
var migrations = new List<Migration>();
foreach (var migrationId in branch.MigrationIds)
{
var migration = _migrationRepository.GetById(migrationId);
if (migration?.DbContextName == dbContextName)
{
var identifier = migration.Name ?? migration.Id;
if (IsIgnored(identifier))
{
_logger.LogInformation("Skipping ignored migration {Migration}", identifier);
continue;
}
migrations.Add(migration);
}
}
return migrations.OrderBy(m => m.Sequence).ToList();
}
/// <summary>
/// Determines whether a migration name matches any of the ignore globs.
/// </summary>
private bool IsIgnored(string? migrationName)
{
if (string.IsNullOrEmpty(migrationName) || _options?.IgnoredMigrations == null)
return false;
foreach (var pattern in _options.IgnoredMigrations)
{
if (GlobMatch(migrationName, pattern))
return true;
}
return false;
}
/// <summary>
/// Simple glob matching where '*' matches any sequence of characters.
/// Case‑insensitive.
/// </summary>
private bool GlobMatch(string text, string pattern)
{
var regexPattern = "^" + Regex.Escape(pattern).Replace("\\*", ".*") + "$";
return Regex.IsMatch(text, regexPattern, RegexOptions.IgnoreCase);
}
/// <summary>
/// Categorizes migrations into source‑only, target‑only, and common.
/// </summary>
private void CategorizeMigrations(
List<Migration> sourceMigrations,
List<Migration> targetMigrations,
MigrationDiff diff)
{
var targetIds = new HashSet<string>(targetMigrations.Select(m => m.Id));
var sourceIds = new HashSet<string>(sourceMigrations.Select(m => m.Id));
foreach (var migration in sourceMigrations)
{
if (targetIds.Contains(migration.Id))
{
diff.AddCommonMigration(migration);
}
else
{
diff.AddSourceOnlyMigration(migration);
}
}
foreach (var migration in targetMigrations)
{
if (!sourceIds.Contains(migration.Id))
{
diff.AddTargetOnlyMigration(migration);
}
}
}
/// <summary>
/// Generates a detailed comparison report suitable for console or text display.
/// Includes source‑only and target‑only migration lists, common migration counts,
/// conflict details with blocking status, and schema change statistics.
/// </summary>
/// <param name="diff">The migration diff result to generate the report from.</param>
/// <returns>A formatted multi‑line string containing the full comparison report.</returns>
public string GenerateReport(MigrationDiff diff)
{
var report = new System.Text.StringBuilder();
report.AppendLine("=== Migration Diff Report ===");
report.AppendLine($"Result: {diff.GetResultDescription()}");
report.AppendLine();
report.AppendLine($"Source Only Migrations: {diff.OnlyInSource.Count}");
foreach (var migration in diff.OnlyInSource)
{
report.AppendLine($" - {migration.Name}");
}
report.AppendLine();
report.AppendLine($"Target Only Migrations: {diff.OnlyInTarget.Count}");
foreach (var migration in diff.OnlyInTarget)
{
report.AppendLine($" - {migration.Name}");
}
report.AppendLine();
report.AppendLine($"Common Migrations: {diff.InBoth.Count}");
report.AppendLine();
if (diff.HasConflicts())
{
report.AppendLine($"Conflicts Detected: {diff.Conflicts.Count}");
report.AppendLine($"Blocking Conflicts: {diff.GetBlockingConflicts()}");
foreach (var conflict in diff.Conflicts)
{
report.AppendLine($" - {conflict}");
}
report.AppendLine();
}
report.AppendLine($"Schema Changes: {diff.GetTotalSchemaChanges()}");
report.AppendLine($"Destructive Changes: {diff.GetDestructiveChanges().Count}");
return report.ToString();
}
}