Skip to content

Commit dd2b46f

Browse files
committed
feat: detect reverted commits and mark them as reverted in changelog (e.g. crossed out in markdown)
1 parent 83ec0f1 commit dd2b46f

6 files changed

Lines changed: 149 additions & 9 deletions

File tree

changelog_cli/lib/src/model/changelog_entry.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ class ChangelogEntry extends Equatable {
88
required this.ref,
99
required this.commit,
1010
required this.date,
11+
this.isReverted = false,
12+
this.revertedByRef,
1113
});
1214

1315
final ConventionalCommit conventionalCommit;
1416
final String ref;
1517
final Commit commit;
1618
final DateTime? date;
19+
final bool isReverted;
20+
final String? revertedByRef;
1721

1822
String get message =>
1923
conventionalCommit.description ?? conventionalCommit.header;
@@ -25,7 +29,24 @@ class ChangelogEntry extends Equatable {
2529
ref,
2630
commit,
2731
date,
32+
isReverted,
33+
revertedByRef,
2834
];
35+
36+
/// Creates a copy of this entry with updated revert information
37+
ChangelogEntry copyWith({
38+
bool? isReverted,
39+
String? revertedByRef,
40+
}) {
41+
return ChangelogEntry(
42+
conventionalCommit: conventionalCommit,
43+
ref: ref,
44+
commit: commit,
45+
date: date,
46+
isReverted: isReverted ?? this.isReverted,
47+
revertedByRef: revertedByRef ?? this.revertedByRef,
48+
);
49+
}
2950
}
3051

3152
class ChangelogEntryGroup extends Equatable {

changelog_cli/lib/src/printers/markdown_printer.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ class MarkdownPrinter extends Printer {
3030
buffer.writeln();
3131
for (final entry in group.entries) {
3232
buffer.write('- ');
33+
if (entry.isReverted){
34+
buffer.write('~');
35+
}
3336
if (entry.conventionalCommit.scopes.isNotEmpty) {
3437
final scopes = entry.conventionalCommit.scopes.join(', ');
3538
buffer.write('**$scopes**: ');
@@ -45,6 +48,10 @@ class MarkdownPrinter extends Printer {
4548
},
4649
);
4750
}
51+
52+
if (entry.isReverted) {
53+
message = '$message~ *(reverted in ${entry.revertedByRef?.substring(0, 7) ?? 'unknown'})*';
54+
}
4855

4956
if (entry.date != null && configuration.dateFormat.isNotEmpty) {
5057
buffer.write(message);

changelog_cli/lib/src/printers/simple_printer.dart

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,18 @@ class SimplePrinter extends Printer {
3434
final scopes = entry.conventionalCommit.scopes.join(', ');
3535
buffer.write('$scopes: ');
3636
}
37+
38+
var message = entry.message;
39+
if (entry.isReverted) {
40+
message = '$message (reverted in ${entry.revertedByRef?.substring(0, 7) ?? 'unknown'})';
41+
}
42+
3743
if (entry.date != null && configuration.dateFormat.isNotEmpty) {
38-
buffer.write(entry.message);
44+
buffer.write(message);
3945
final dateFormatted = configuration.formatDateTime(entry.date);
4046
buffer.writeln(' ($dateFormatted)');
4147
} else {
42-
buffer.writeln(entry.message);
48+
buffer.writeln(message);
4349
}
4450
}
4551
buffer.writeln();

changelog_cli/lib/src/printers/slack_markdown_printer.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ class SlackMarkdownPrinter extends Printer {
3535
buffer.writeln();
3636
for (final entry in group.entries) {
3737
buffer.write('- ');
38+
if (entry.isReverted){
39+
buffer.write('~');
40+
}
3841
if (entry.conventionalCommit.scopes.isNotEmpty) {
3942
final scopes = entry.conventionalCommit.scopes.join(', ');
4043
buffer.write('*$scopes*: ');
@@ -50,6 +53,10 @@ class SlackMarkdownPrinter extends Printer {
5053
},
5154
);
5255
}
56+
57+
if (entry.isReverted) {
58+
message = '$message~ _(reverted in `${entry.revertedByRef?.substring(0, 7) ?? 'unknown'}`)_';
59+
}
5360

5461
if (entry.date != null && configuration.dateFormat.isNotEmpty) {
5562
buffer.write(message);

changelog_cli/lib/src/processors/processors.dart

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:changelog_cli/src/model/model.dart';
2+
import 'package:changelog_cli/src/processors/revert_detector.dart';
23
import 'package:collection/collection.dart';
34
import 'package:mason_logger/mason_logger.dart';
45

@@ -10,11 +11,13 @@ class Preprocessor {
1011
GenerateConfiguration configuration, {
1112
Logger? logger,
1213
}) {
14+
final entriesWithReverts = RevertDetector.detectReverts(entries, logger: logger);
15+
1316
// group by configuration.groupBy
1417
logger?.detail('Grouping changelog entries by ${configuration.groupBy}');
1518
switch (configuration.groupBy) {
1619
case GroupBy.dateAsc:
17-
entries.sort((a, b) {
20+
entriesWithReverts.sort((ChangelogEntry a, ChangelogEntry b) {
1821
if (a.date == null && b.date == null) {
1922
return 0;
2023
}
@@ -27,7 +30,7 @@ class Preprocessor {
2730
return a.date!.compareTo(b.date!);
2831
});
2932
case GroupBy.dateDesc:
30-
entries.sort((a, b) {
33+
entriesWithReverts.sort((ChangelogEntry a, ChangelogEntry b) {
3134
if (a.date == null && b.date == null) {
3235
return 0;
3336
}
@@ -40,18 +43,20 @@ class Preprocessor {
4043
return b.date!.compareTo(a.date!);
4144
});
4245
case GroupBy.scopeAsc:
43-
entries.sort(
44-
(a, b) => a.conventionalCommit.scopes.join().compareTo(b.conventionalCommit.scopes.join()),
46+
entriesWithReverts.sort(
47+
(ChangelogEntry a, ChangelogEntry b) =>
48+
a.conventionalCommit.scopes.join().compareTo(b.conventionalCommit.scopes.join()),
4549
);
4650
case GroupBy.scopeDesc:
47-
entries.sort(
48-
(a, b) => b.conventionalCommit.scopes.join().compareTo(a.conventionalCommit.scopes.join()),
51+
entriesWithReverts.sort(
52+
(ChangelogEntry a, ChangelogEntry b) =>
53+
b.conventionalCommit.scopes.join().compareTo(a.conventionalCommit.scopes.join()),
4954
);
5055
}
5156

5257
// depending on the grouping:
5358

54-
final groupedEntries = entries.groupListsBy((e) => e.type);
59+
final groupedEntries = entriesWithReverts.groupListsBy((ChangelogEntry e) => e.type);
5560

5661
final filteredEntries = <ChangelogEntryGroup>[];
5762

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import 'package:changelog_cli/src/model/model.dart';
2+
import 'package:mason_logger/mason_logger.dart';
3+
4+
class RevertDetector {
5+
/// Detects reverted commits in the changelog entries and marks them appropriately
6+
static List<ChangelogEntry> detectReverts(
7+
List<ChangelogEntry> entries, {
8+
Logger? logger,
9+
}) {
10+
final result = <ChangelogEntry>[];
11+
final commitRefToEntry = <String, ChangelogEntry>{};
12+
13+
// Build a map of commit refs to entries for quick lookup
14+
for (final entry in entries) {
15+
commitRefToEntry[entry.ref] = entry;
16+
}
17+
18+
logger?.detail('Detecting reverted commits in ${entries.length} entries');
19+
20+
for (final entry in entries) {
21+
final revertInfo = parseRevertCommit(entry.commit.message);
22+
23+
if (revertInfo != null) {
24+
// This is a revert commit
25+
logger?.detail('Found revert commit ${entry.ref}: ${revertInfo.revertedCommitRef}');
26+
27+
// Find the original commit that was reverted
28+
final revertedEntry = commitRefToEntry[revertInfo.revertedCommitRef];
29+
if (revertedEntry != null) {
30+
// Mark the original commit as reverted
31+
final updatedRevertedEntry = revertedEntry.copyWith(
32+
isReverted: true,
33+
revertedByRef: entry.ref,
34+
);
35+
commitRefToEntry[revertedEntry.ref] = updatedRevertedEntry;
36+
logger?.detail('Marked commit ${revertedEntry.ref} as reverted by ${entry.ref}');
37+
} else {
38+
logger?.detail('Could not find original commit ${revertInfo.revertedCommitRef} to mark as reverted');
39+
}
40+
41+
// Skip revert commits - they shouldn't appear in the changelog
42+
logger?.detail('Skipping revert commit ${entry.ref} from changelog');
43+
} else {
44+
// Regular commit, add the potentially updated version from our map
45+
final updatedEntry = commitRefToEntry[entry.ref] ?? entry;
46+
result.add(updatedEntry);
47+
}
48+
}
49+
50+
final revertedCount = result.where((e) => e.isReverted).length;
51+
logger?.detail('Found $revertedCount reverted commits');
52+
53+
return result;
54+
}
55+
56+
/// Parses a commit message to extract revert information
57+
static RevertInfo? parseRevertCommit(String commitMessage) {
58+
// Common revert message patterns
59+
final revertPatterns = [
60+
// Git's default revert format: "Revert "commit title""
61+
RegExp(r'^Revert\s+".*"\s*\n\nThis reverts commit\s+([a-f0-9]{7,40})\.?', caseSensitive: false, multiLine: true),
62+
63+
// Alternative format: "Revert commit abc1234"
64+
RegExp(r'^Revert\s+commit\s+([a-f0-9]{7,40})', caseSensitive: false, multiLine: true),
65+
66+
// GitHub style: "Revert #PR-number" or similar patterns
67+
RegExp(r'^Revert\s+".*"\s*.*commit\s+([a-f0-9]{7,40})', caseSensitive: false, multiLine: true, dotAll: true),
68+
69+
// Manual revert with "This reverts commit" somewhere in message
70+
RegExp(r'This reverts commit\s+([a-f0-9]{7,40})\.?', caseSensitive: false, multiLine: true),
71+
];
72+
73+
for (final pattern in revertPatterns) {
74+
final match = pattern.firstMatch(commitMessage);
75+
if (match != null) {
76+
final revertedCommitRef = match.group(1);
77+
if (revertedCommitRef != null) {
78+
return RevertInfo(revertedCommitRef: revertedCommitRef);
79+
}
80+
}
81+
}
82+
83+
return null;
84+
}
85+
}
86+
87+
/// Information about a revert commit
88+
class RevertInfo {
89+
const RevertInfo({
90+
required this.revertedCommitRef,
91+
});
92+
93+
final String revertedCommitRef;
94+
}

0 commit comments

Comments
 (0)