Skip to content

Commit f1fbee9

Browse files
alex-slynkoCopilot
andcommitted
Group identical cluster diffs in markdown output
When multiple clusters have the same changes, show them once with a combined header instead of repeating the full diff for each cluster. Uses a canonical fingerprint (markdown + comments + validity) to detect identical diffs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a6fdd05 commit f1fbee9

2 files changed

Lines changed: 203 additions & 19 deletions

File tree

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
using KustoSchemaTools.Changes;
2+
using KustoSchemaTools.Model;
3+
4+
namespace KustoSchemaTools.Tests.Changes
5+
{
6+
public class ClusterGroupingTests
7+
{
8+
[Fact]
9+
public void BuildClusterFingerprint_IdenticalChanges_ProduceSameFingerprint()
10+
{
11+
var changes1 = CreateSampleChanges("## Table1\nSome diff");
12+
var changes2 = CreateSampleChanges("## Table1\nSome diff");
13+
var comments = new List<Comment>();
14+
15+
var fp1 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes1, comments, true);
16+
var fp2 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes2, comments, true);
17+
18+
Assert.Equal(fp1, fp2);
19+
}
20+
21+
[Fact]
22+
public void BuildClusterFingerprint_DifferentMarkdown_ProduceDifferentFingerprints()
23+
{
24+
var changes1 = CreateSampleChanges("## Table1\nDiff A");
25+
var changes2 = CreateSampleChanges("## Table1\nDiff B");
26+
var comments = new List<Comment>();
27+
28+
var fp1 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes1, comments, true);
29+
var fp2 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes2, comments, true);
30+
31+
Assert.NotEqual(fp1, fp2);
32+
}
33+
34+
[Fact]
35+
public void BuildClusterFingerprint_DifferentValidity_ProduceDifferentFingerprints()
36+
{
37+
var changes = CreateSampleChanges("## Table1\nSame diff");
38+
var comments = new List<Comment>();
39+
40+
var fp1 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments, true);
41+
var fp2 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments, false);
42+
43+
Assert.NotEqual(fp1, fp2);
44+
}
45+
46+
[Fact]
47+
public void BuildClusterFingerprint_DifferentComments_ProduceDifferentFingerprints()
48+
{
49+
var changes = CreateSampleChanges("## Table1\nSame diff");
50+
var comments1 = new List<Comment>
51+
{
52+
new Comment { Kind = CommentKind.Warning, Text = "Warning 1", FailsRollout = false }
53+
};
54+
var comments2 = new List<Comment>
55+
{
56+
new Comment { Kind = CommentKind.Caution, Text = "Caution 1", FailsRollout = true }
57+
};
58+
59+
var fp1 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments1, true);
60+
var fp2 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments2, true);
61+
62+
Assert.NotEqual(fp1, fp2);
63+
}
64+
65+
[Fact]
66+
public void BuildClusterFingerprint_EmptyChanges_ProduceSameFingerprint()
67+
{
68+
var changes = new List<IChange>();
69+
var comments = new List<Comment>();
70+
71+
var fp1 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments, true);
72+
var fp2 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments, true);
73+
74+
Assert.Equal(fp1, fp2);
75+
}
76+
77+
[Fact]
78+
public void BuildClusterFingerprint_CommentsInDifferentOrder_ProduceSameFingerprint()
79+
{
80+
var changes = CreateSampleChanges("## Table1\nSame diff");
81+
var comments1 = new List<Comment>
82+
{
83+
new Comment { Kind = CommentKind.Warning, Text = "A", FailsRollout = false },
84+
new Comment { Kind = CommentKind.Note, Text = "B", FailsRollout = false }
85+
};
86+
var comments2 = new List<Comment>
87+
{
88+
new Comment { Kind = CommentKind.Note, Text = "B", FailsRollout = false },
89+
new Comment { Kind = CommentKind.Warning, Text = "A", FailsRollout = false }
90+
};
91+
92+
var fp1 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments1, true);
93+
var fp2 = KustoSchemaHandler<Database>.BuildClusterFingerprint(changes, comments2, true);
94+
95+
Assert.Equal(fp1, fp2);
96+
}
97+
98+
private static List<IChange> CreateSampleChanges(string markdown)
99+
{
100+
return new List<IChange>
101+
{
102+
new FakeChange(markdown)
103+
};
104+
}
105+
106+
private class FakeChange : IChange
107+
{
108+
public FakeChange(string markdown)
109+
{
110+
Markdown = markdown;
111+
}
112+
113+
public string EntityType => "Test";
114+
public string Entity => "TestEntity";
115+
public List<DatabaseScriptContainer> Scripts => new List<DatabaseScriptContainer>();
116+
public string Markdown { get; }
117+
public Comment Comment { get; set; }
118+
}
119+
}
120+
}

KustoSchemaTools/KustoSchemaHandler.cs

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -147,49 +147,73 @@ private async Task<DiffComputationResult> BuildDiffComputationResult(string path
147147
var sb = new StringBuilder();
148148
bool isValid = true;
149149

150+
// Compute per-cluster metadata first (for logging and validity)
151+
var clusterRenderModels = new List<ClusterRenderModel>();
150152
foreach (var clusterDiff in diffData.ClusterDiffs)
151153
{
154+
var changes = clusterDiff.Changes;
155+
var comments = changes.Select(change => change.Comment).OfType<Comment>().ToList();
156+
var clusterValid = IsDiffValid(changes);
157+
isValid &= clusterValid;
158+
159+
var fingerprint = BuildClusterFingerprint(changes, comments, clusterValid);
160+
161+
clusterRenderModels.Add(new ClusterRenderModel(clusterDiff, changes, comments, clusterValid, fingerprint));
162+
152163
if (logDetails)
153164
{
154165
Log.LogInformation($"Generating diff markdown for {Path.Join(path, databaseName)} => {clusterDiff.Cluster.Name}/{databaseName}");
166+
var scriptSb = new StringBuilder();
167+
foreach (var script in changes.SelectMany(itm => itm.Scripts).Where(itm => itm.IsValid is true).OrderBy(itm => itm.Script.Order))
168+
{
169+
scriptSb.AppendLine(script.Script.Text);
170+
}
171+
Log.LogInformation($"Following scripts will be applied:\n{scriptSb}");
155172
}
173+
}
156174

157-
var changes = clusterDiff.Changes;
158-
var comments = changes.Select(change => change.Comment).OfType<Comment>().ToList();
159-
var clusterValid = IsDiffValid(changes);
160-
isValid &= clusterValid;
175+
// Group clusters with identical diffs
176+
var groups = clusterRenderModels.GroupBy(m => m.Fingerprint).ToList();
161177

162-
sb.AppendLine($"# {clusterDiff.Cluster.Name}/{databaseName} ({clusterDiff.Cluster.Url})");
178+
foreach (var group in groups)
179+
{
180+
var clusters = group.ToList();
181+
var representative = clusters.First();
182+
183+
// Build combined header
184+
var clusterHeaders = clusters.Select(c => $"{c.Context.Cluster.Name}/{databaseName} ({c.Context.Cluster.Url})");
185+
if (clusters.Count == 1)
186+
{
187+
sb.AppendLine($"# {clusterHeaders.First()}");
188+
}
189+
else
190+
{
191+
sb.AppendLine($"# {clusters.Count} clusters with identical changes");
192+
foreach (var header in clusterHeaders)
193+
{
194+
sb.AppendLine($"- {header}");
195+
}
196+
sb.AppendLine();
197+
}
163198

164-
foreach (var comment in comments)
199+
foreach (var comment in representative.Comments)
165200
{
166201
sb.AppendLine($"> [!{comment.Kind.ToString().ToUpper()}]");
167202
sb.AppendLine($"> {comment.Text}");
168203
sb.AppendLine();
169204
}
170205

171-
if (changes.Count == 0)
206+
if (representative.Changes.Count == 0)
172207
{
173208
sb.AppendLine("No changes detected");
174209
}
175210

176-
foreach (var change in changes)
211+
foreach (var change in representative.Changes)
177212
{
178213
sb.AppendLine(change.Markdown);
179214
sb.AppendLine();
180215
sb.AppendLine();
181216
}
182-
183-
if (logDetails)
184-
{
185-
var scriptSb = new StringBuilder();
186-
foreach (var script in changes.SelectMany(itm => itm.Scripts).Where(itm => itm.IsValid is true).OrderBy(itm => itm.Script.Order))
187-
{
188-
scriptSb.AppendLine(script.Script.Text);
189-
}
190-
191-
Log.LogInformation($"Following scripts will be applied:\n{scriptSb}");
192-
}
193217
}
194218

195219
foreach (var followerDiff in diffData.FollowerDiffs)
@@ -214,6 +238,46 @@ private async Task<DiffComputationResult> BuildDiffComputationResult(string path
214238
return (sb.ToString(), isValid);
215239
}
216240

241+
/// <summary>
242+
/// Builds a canonical fingerprint for a cluster's diff output.
243+
/// Clusters with the same fingerprint will be grouped together in the markdown.
244+
/// </summary>
245+
public static string BuildClusterFingerprint(List<IChange> changes, List<Comment> comments, bool isValid)
246+
{
247+
var sb = new StringBuilder();
248+
sb.Append($"valid:{isValid};");
249+
250+
foreach (var comment in comments.OrderBy(c => c.Kind).ThenBy(c => c.Text))
251+
{
252+
sb.Append($"comment:{comment.Kind}:{comment.FailsRollout}:{comment.Text};");
253+
}
254+
255+
foreach (var change in changes)
256+
{
257+
sb.Append($"change:{change.Markdown};");
258+
}
259+
260+
return sb.ToString();
261+
}
262+
263+
private sealed class ClusterRenderModel
264+
{
265+
public ClusterRenderModel(ClusterDiffContext context, List<IChange> changes, List<Comment> comments, bool isValid, string fingerprint)
266+
{
267+
Context = context;
268+
Changes = changes;
269+
Comments = comments;
270+
IsValid = isValid;
271+
Fingerprint = fingerprint;
272+
}
273+
274+
public ClusterDiffContext Context { get; }
275+
public List<IChange> Changes { get; }
276+
public List<Comment> Comments { get; }
277+
public bool IsValid { get; }
278+
public string Fingerprint { get; }
279+
}
280+
217281
private StructuredDiff ConvertToStructuredDiff(string clusterName, string clusterUrl, string databaseName, List<IChange> changes)
218282
{
219283
var structuredChanges = changes.Select(change => change.ToStructuredChange()).ToList();

0 commit comments

Comments
 (0)