Skip to content

Commit e36bf94

Browse files
committed
Handle additional text inside changelog
1 parent 1efc7e7 commit e36bf94

File tree

5 files changed

+146
-18
lines changed

5 files changed

+146
-18
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## [Unreleased]
44

5+
### Fixed
6+
7+
- Fixed an issue where free‑form text outside of list items was removed during `patchChangelog` task
8+
JetBrains/gradle-changelog-plugin/#285
9+
510
## [2.5.0] - 2025-11-25
611

712
### Added

api/gradle-changelog-plugin.api

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ public final class org/jetbrains/changelog/Changelog {
2020
}
2121

2222
public final class org/jetbrains/changelog/Changelog$Item {
23-
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;)V
24-
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
23+
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;Ljava/lang/String;)V
24+
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
2525
public final fun component1 ()Ljava/lang/String;
2626
public final fun component2 ()Ljava/lang/String;
2727
public final fun component3 ()Ljava/lang/String;
2828
public final fun component4 ()Z
29-
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;)Lorg/jetbrains/changelog/Changelog$Item;
30-
public static synthetic fun copy$default (Lorg/jetbrains/changelog/Changelog$Item;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;ILjava/lang/Object;)Lorg/jetbrains/changelog/Changelog$Item;
29+
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;Ljava/lang/String;)Lorg/jetbrains/changelog/Changelog$Item;
30+
public static synthetic fun copy$default (Lorg/jetbrains/changelog/Changelog$Item;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZLjava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lorg/jetbrains/changelog/Changelog$Item;
3131
public fun equals (Ljava/lang/Object;)Z
3232
public final fun getHeader ()Ljava/lang/String;
3333
public final fun getSections ()Ljava/util/Map;

src/main/kotlin/org/jetbrains/changelog/Changelog.kt

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ data class Changelog(
135135
}
136136
val isUnreleased = key == unreleasedTerm
137137

138-
val (summary, items) = value.extractItemData()
138+
val (summary, items, suffix) = value.extractItemData()
139139

140-
Item(key, header, summary, isUnreleased, items)
140+
Item(key, header, summary, isUnreleased, items, suffix)
141141
.withEmptySections(isUnreleased)
142142
}
143143
}
@@ -250,6 +250,11 @@ data class Changelog(
250250
}
251251
}
252252

253+
if (suffix.isNotEmpty()) {
254+
add(lineSeparator)
255+
add(suffix)
256+
}
257+
253258
if (withLinks) {
254259
links
255260
.filterKeys { id ->
@@ -300,6 +305,7 @@ data class Changelog(
300305
val summary: String = "",
301306
val isUnreleased: Boolean = false,
302307
private val items: Map<String, Set<String>> = mutableMapOf(),
308+
internal val suffix: String = "",
303309
) {
304310

305311
internal var withHeader = true
@@ -333,13 +339,14 @@ data class Changelog(
333339
summary: String = this.summary,
334340
isUnreleased: Boolean = this.isUnreleased,
335341
items: Map<String, Set<String>> = this.items,
342+
suffix: String = this.suffix,
336343
withHeader: Boolean = this.withHeader,
337344
withLinkedHeader: Boolean = this.withLinkedHeader,
338345
withSummary: Boolean = this.withSummary,
339346
withLinks: Boolean = this.withLinks,
340347
withEmptySections: Boolean = this.withEmptySections,
341348
filterCallback: ((String) -> Boolean)? = this.filterCallback,
342-
) = Item(version, header, summary, isUnreleased, items).also {
349+
) = Item(version, header, summary, isUnreleased, items, suffix).also {
343350
it.withHeader = withHeader
344351
it.withLinkedHeader = withLinkedHeader
345352
it.withSummary = withSummary
@@ -356,6 +363,7 @@ data class Changelog(
356363
return copy(
357364
summary = summary.ifEmpty { item.summary },
358365
items = items + item.items,
366+
suffix = listOf(suffix, item.suffix).filter { it.isNotEmpty() }.joinToString("\n\n"),
359367
filterCallback = filterCallback,
360368
)
361369
}
@@ -432,15 +440,20 @@ data class Changelog(
432440

433441
private fun List<ASTNode>.join() = joinToString(lineSeparator) { it.text() }
434442

435-
internal fun List<ASTNode>.extractItemData(content: String? = null): Pair<String, Map<String, Set<String>>> {
436-
val summaryNodes = this
437-
.filter { it.type != ATX_2 }
443+
internal data class ItemData(
444+
val summary: String,
445+
val items: Map<String, Set<String>>,
446+
val suffix: String,
447+
)
448+
449+
internal fun List<ASTNode>.extractItemData(content: String? = null): ItemData {
450+
val nodesWithoutHeader = this.filter { it.type != ATX_2 }
451+
452+
val summaryNodes = nodesWithoutHeader
438453
.takeWhile { it.type == PARAGRAPH }
439454
val summary = summaryNodes
440455
.joinToString("$lineSeparator$lineSeparator") { it.text(content) }
441-
442-
val sectionNodes = this
443-
.filter { it.type != ATX_2 }
456+
val sectionNodes = nodesWithoutHeader
444457
.filter { it.type == ATX_3 || it.type == UNORDERED_LIST || it.type == ORDERED_LIST }
445458

446459
val items = with(sectionNodes) {
@@ -479,7 +492,28 @@ data class Changelog(
479492
unassignedItems + sectionPlaceholders + sectionsWithItems
480493
}
481494

482-
return summary to items
495+
// Capture any extra text (paragraphs, tables, ordered lists, etc.) that falls
496+
// outside structured release notes by reading raw source after the last node
497+
// that was actually processed above.
498+
val rawContent = content ?: this@Changelog.content
499+
val lastSectionNode = sectionNodes.lastOrNull { it.type == ATX_3 }
500+
val lastProcessedNode = if (lastSectionNode != null) {
501+
sectionNodes.firstOrNull { it.startOffset > lastSectionNode.endOffset } ?: lastSectionNode
502+
} else {
503+
sectionNodes.lastOrNull()
504+
}
505+
// Fall back to summary paragraphs
506+
val endOfProcessed = lastProcessedNode?.endOffset
507+
?: summaryNodes.lastOrNull()?.endOffset
508+
509+
val blockEnd = this.lastOrNull { it.type != LINK_DEFINITION }?.endOffset
510+
val suffix = if (endOfProcessed != null && blockEnd != null && endOfProcessed < blockEnd) {
511+
rawContent.substring(endOfProcessed, blockEnd).trim()
512+
} else {
513+
""
514+
}
515+
516+
return ItemData(summary, items, suffix)
483517
}
484518

485519
internal fun List<ASTNode>.extractLinks(content: String? = null) = this

src/main/kotlin/org/jetbrains/changelog/tasks/PatchChangelogTask.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,14 +103,15 @@ abstract class PatchChangelogTask : BaseChangelogTask() {
103103
isUnreleased = false,
104104
).let {
105105
parseTree(releaseNoteContent)?.let { releaseNoteTree ->
106-
val (summary, items) = releaseNoteTree.children.extractItemData(releaseNoteContent)
106+
val itemData = releaseNoteTree.children.extractItemData(releaseNoteContent)
107107
val links = releaseNoteTree.children.extractLinks(releaseNoteContent)
108108

109109
baseLinks.addAll(links)
110110

111111
it.copy(
112-
summary = summary,
113-
items = items,
112+
summary = itemData.summary,
113+
items = itemData.items,
114+
suffix = itemData.suffix,
114115
)
115116
} ?: it
116117
} + preReleaseItems

src/test/kotlin/org/jetbrains/changelog/tasks/PatchChangelogTaskTest.kt

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import org.jetbrains.changelog.exceptions.MissingVersionException
88
import org.jetbrains.changelog.normalizeLineSeparator
99
import java.text.SimpleDateFormat
1010
import java.util.*
11-
import kotlin.test.*
11+
import kotlin.test.BeforeTest
12+
import kotlin.test.Test
13+
import kotlin.test.assertEquals
14+
import kotlin.test.assertFailsWith
15+
import kotlin.test.assertFalse
16+
import kotlin.test.assertTrue
1217

1318
class PatchChangelogTaskTest : BaseTest() {
1419

@@ -1715,4 +1720,87 @@ class PatchChangelogTaskTest : BaseTest() {
17151720

17161721
assertMarkdown(expectedContent, changelog)
17171722
}
1723+
1724+
@Test
1725+
fun `additional text inside release notes`() {
1726+
changelog =
1727+
"""
1728+
# Gradle plugin changelog
1729+
1730+
## Unreleased
1731+
1732+
## 0.4.0 - 2026-02-25
1733+
1734+
### Added
1735+
1736+
- Some release notes
1737+
1738+
Additional note for customers.
1739+
1740+
Structured info:
1741+
1742+
1. **L1** - text
1743+
2. **L2** - text
1744+
3. **L3** - text
1745+
1746+
## 0.3.0 - 2026-02-24
1747+
1748+
### Changed
1749+
1750+
- Introduce breaking changes
1751+
1752+
| Before | After |
1753+
|--------|-------------|
1754+
| `test` | `test test` |
1755+
1756+
""".trimIndent()
1757+
1758+
buildFile =
1759+
"""
1760+
plugins {
1761+
id 'org.jetbrains.changelog'
1762+
}
1763+
changelog {
1764+
version = "0.4.0"
1765+
groups.empty()
1766+
}
1767+
""".trimIndent()
1768+
project.evaluate()
1769+
runTask(PATCH_CHANGELOG_TASK_NAME)
1770+
1771+
val expectedContent =
1772+
"""
1773+
# Gradle plugin changelog
1774+
1775+
## Unreleased
1776+
1777+
## 0.4.0 - 2026-02-25
1778+
1779+
### Added
1780+
1781+
- Some release notes
1782+
1783+
Additional note for customers.
1784+
1785+
Structured info:
1786+
1787+
1. **L1** - text
1788+
2. **L2** - text
1789+
3. **L3** - text
1790+
1791+
## 0.3.0 - 2026-02-24
1792+
1793+
### Changed
1794+
1795+
- Introduce breaking changes
1796+
1797+
| Before | After |
1798+
|--------|-------------|
1799+
| `test` | `test test` |
1800+
1801+
""".trimIndent()
1802+
1803+
1804+
assertMarkdown(expectedContent, changelog)
1805+
}
17181806
}

0 commit comments

Comments
 (0)