@@ -200,18 +200,18 @@ fun generateChangelog() {
200
200
append(
201
201
"""
202
202
## Dependencies
203
-
203
+
204
204
- Gradle Plugin `org.jetbrains.compose`, version `$versionCompose `. Based on Jetpack Compose libraries:
205
205
- [Runtime $versionRedirectingCompose ](https://developer.android.com/jetpack/androidx/releases/compose-runtime#$versionRedirectingCompose )
206
206
- [UI $versionRedirectingCompose ](https://developer.android.com/jetpack/androidx/releases/compose-ui#$versionRedirectingCompose )
207
207
- [Foundation $versionRedirectingCompose ](https://developer.android.com/jetpack/androidx/releases/compose-foundation#$versionRedirectingCompose )
208
208
- [Material $versionRedirectingCompose ](https://developer.android.com/jetpack/androidx/releases/compose-material#$versionRedirectingCompose )
209
209
- [Material3 $versionRedirectingComposeMaterial3 ](https://developer.android.com/jetpack/androidx/releases/compose-material3#$versionRedirectingComposeMaterial3 )
210
-
210
+
211
211
- Lifecycle libraries `org.jetbrains.androidx.lifecycle:lifecycle-*:$versionLifecycle `. Based on [Jetpack Lifecycle $versionRedirectingLifecycle ](https://developer.android.com/jetpack/androidx/releases/lifecycle#$versionRedirectingLifecycle )
212
212
- Navigation libraries `org.jetbrains.androidx.navigation:navigation-*:$versionNavigation `. Based on [Jetpack Navigation $versionRedirectingNavigation ](https://developer.android.com/jetpack/androidx/releases/navigation#$versionRedirectingNavigation )
213
213
- Material3 Adaptive libraries `org.jetbrains.compose.material3.adaptive:adaptive*:$versionComposeMaterial3Adaptive `. Based on [Jetpack Material3 Adaptive $versionRedirectingComposeMaterial3Adaptive ](https://developer.android.com/jetpack/androidx/releases/compose-material3-adaptive#$versionRedirectingComposeMaterial3Adaptive )
214
-
214
+
215
215
---
216
216
""" .trimIndent()
217
217
)
@@ -267,7 +267,7 @@ fun checkPr() {
267
267
releaseNotes is ReleaseNotes .Specified && releaseNotes.entries.isEmpty() -> {
268
268
err.println ("""
269
269
"## Release Notes" doesn't contain any items, or "### Section - Subsection" isn't specified
270
-
270
+
271
271
See the format in $prFormatLink
272
272
""" .trimIndent())
273
273
exitProcess(1 )
@@ -276,18 +276,18 @@ fun checkPr() {
276
276
err.println ("""
277
277
"## Release Notes" contains nonstandard "Section - Subsection" pairs:
278
278
${nonstandardSections.joinToString(" , " )}
279
-
279
+
280
280
Allowed sections: ${standardSections.joinToString(" , " )}
281
281
Allowed subsections: ${standardSubsections.joinToString(" , " )}
282
-
282
+
283
283
See the full format in $prFormatLink
284
284
""" .trimIndent())
285
285
exitProcess(1 )
286
286
}
287
287
releaseNotes == null -> {
288
288
err.println ("""
289
289
"## Release Notes" section is missing in the PR description
290
-
290
+
291
291
See the format in $prFormatLink
292
292
""" .trimIndent())
293
293
exitProcess(1 )
@@ -303,6 +303,11 @@ fun checkPr() {
303
303
*/
304
304
fun currentChangelogDate () = LocalDate .now().format(DateTimeFormatter .ofPattern(" MMMM yyyy" , Locale .ENGLISH ))
305
305
306
+ fun GitHubPullEntry.extractReleaseNotes () = extractReleaseNotes(body, number, htmlUrl)
307
+
308
+ fun GitHubPullEntry.unknownChangelogEntries () =
309
+ listOf (ChangelogEntry (" - $title " , null , null , number, htmlUrl, false ))
310
+
306
311
/* *
307
312
* Extract by format [PR_FORMAT.md]
308
313
*/
@@ -330,6 +335,14 @@ fun extractReleaseNotes(body: String?, prNumber: Int, prLink: String): ReleaseNo
330
335
if (relNoteBody == null ) return null
331
336
if (relNoteBody.trim().lowercase() == " n/a" ) return ReleaseNotes .NA
332
337
338
+ // Check if the release notes contain only GitHub PR links
339
+ val pullRequests = relNoteBody
340
+ .split(" \n " )
341
+ .map { it.trim() }
342
+ .mapNotNull(PullRequestLink ::parseOrNull)
343
+
344
+ if (pullRequests.isNotEmpty()) return ReleaseNotes .CherryPicks (pullRequests)
345
+
333
346
val list = mutableListOf<ChangelogEntry >()
334
347
var section: String? = null
335
348
var subsection: String? = null
@@ -381,40 +394,44 @@ fun extractReleaseNotes(body: String?, prNumber: Int, prLink: String): ReleaseNo
381
394
* JetBrains/compose-multiplatform-core
382
395
*/
383
396
fun entriesForRepo (repo : String , firstCommit : String , lastCommit : String ): List <ChangelogEntry > {
384
- val pulls = (1 .. 5 )
397
+ val pulls = (1 .. 10 )
385
398
.flatMap {
386
399
requestJson<Array <GitHubPullEntry >>(" https://api.github.com/repos/$repo /pulls?state=closed&per_page=100&page=$it " ).toList()
387
400
}
388
401
389
- val shaToOriginalPull = pulls.associateBy { it.mergeCommitSha }
402
+ val shaToPull = pulls.associateBy { it.mergeCommitSha }
390
403
val numberToPull = pulls.associateBy { it.number }
391
- val pullToCherryPickPull = pulls.associateWith {
392
- it.findCherryPickPullNumber()?. let (numberToPull::get)
393
- }
394
-
395
- fun pullOf ( sha : String ): GitHubPullEntry ? {
396
- val originalPr = shaToOriginalPull[sha]
397
- return pullToCherryPickPull[originalPr] ? : originalPr
398
- }
399
-
400
- fun changelogEntriesFor ( pullRequest : GitHubPullEntry ) = with (pullRequest) {
401
- extractReleaseNotes(body, number, htmlUrl)?.entries ? :
402
- listOf ( ChangelogEntry ( " - $title " , null , null , number, htmlUrl, false ))
404
+ val pullToReleaseNotes = Cache ( GitHubPullEntry ::extractReleaseNotes)
405
+
406
+ // if GitHubPullEntry is a cherry-picks PR (contains a list of links to other PRs), replace it by the original PRs
407
+ fun List<GitHubPullEntry>. replaceCherryPicks (): List < GitHubPullEntry > = flatMap { pullRequest ->
408
+ val releaseNotes = pullToReleaseNotes[pullRequest]
409
+ if (releaseNotes is ReleaseNotes . CherryPicks ) {
410
+ releaseNotes.pullRequests
411
+ .filter { it.repo == repo }
412
+ .mapNotNull { numberToPull[it.number] }
413
+ } else {
414
+ listOf (pullRequest)
415
+ }
403
416
}
404
417
405
418
val repoFolder = githubClone(repo)
406
419
407
- // Commits that exist in [firstCommit] and not identified as cherry-picks.
408
- // We identify them via reading a PR description "Cherry-picked from ..."
420
+ // Commits that exist in [firstCommit] and not identified as cherry-picks by `git log`
421
+ // We'll try to exclude them via manual links to cherry-picks
409
422
val cherryPickedPrsInFirstCommit = gitLogShas(repoFolder, firstCommit, lastCommit, " --cherry-pick --left-only" )
410
- .mapNotNull(::pullOf)
423
+ .mapNotNull(shaToPull::get)
424
+ .replaceCherryPicks()
411
425
412
426
return gitLogShas(repoFolder, firstCommit, lastCommit, " --cherry-pick --right-only" )
413
427
.reversed() // older changes are at the bottom
414
- .mapNotNull(::pullOf)
428
+ .mapNotNull(shaToPull::get)
429
+ .replaceCherryPicks()
415
430
.minus(cherryPickedPrsInFirstCommit)
416
431
.distinctBy { it.number }
417
- .flatMap(::changelogEntriesFor)
432
+ .flatMap {
433
+ pullToReleaseNotes[it]?.entries ? : it.unknownChangelogEntries()
434
+ }
418
435
}
419
436
420
437
/* *
@@ -443,7 +460,11 @@ fun androidxLibToRedirectionVersion(commit: String): Map<String, String> {
443
460
fun androidxLibToVersion (commit : String ): Map <String , String > {
444
461
val repo
= " ssh://[email protected] /ui/compose-teamcity-config.git"
445
462
val file = " .teamcity/compose/Library.kt"
446
- val libraryKt = spaceContentOf(repo, file, commit)
463
+ val libraryKt = try {
464
+ spaceContentOf(repo, file, commit)
465
+ } catch (_: Exception ) {
466
+ " "
467
+ }
447
468
448
469
return if (libraryKt.isBlank()) {
449
470
println (" Can't find library versions in $repo for $commit . Either the format is changed, or you need to register your ssh key in https://jetbrains.team/m/me/authentication?tab=GitKeys" )
@@ -490,18 +511,34 @@ fun githubClone(repo: String): File {
490
511
pipeProcess(" git clone --bare $url $absolutePath " ).waitAndCheck()
491
512
} else {
492
513
println (" Fetching $url into ${folder.absolutePath} " )
493
- pipeProcess(" git -C $absolutePath fetch" ).waitAndCheck()
514
+ pipeProcess(" git -C $absolutePath fetch --tags " ).waitAndCheck()
494
515
}
495
516
return folder
496
517
}
497
518
519
+ data class PullRequestLink (val repo : String , val number : Int ) {
520
+ companion object {
521
+ fun parseOrNull (link : String ): PullRequestLink ? {
522
+ val (repo, number) = Regex (" https://github\\ .com/(.+)/pull/(\\ d+)/?" )
523
+ .matchEntire(link)
524
+ ?.destructured
525
+ ? : return null
526
+ return PullRequestLink (repo, number.toInt())
527
+ }
528
+ }
529
+ }
530
+
498
531
sealed interface ReleaseNotes {
499
532
val entries: List <ChangelogEntry >
500
533
501
534
object NA: ReleaseNotes {
502
535
override val entries: List <ChangelogEntry > get() = emptyList()
503
536
}
504
537
538
+ class CherryPicks (val pullRequests : List <PullRequestLink >): ReleaseNotes {
539
+ override val entries: List <ChangelogEntry > get() = emptyList()
540
+ }
541
+
505
542
class Specified (override val entries : List <ChangelogEntry >): ReleaseNotes
506
543
}
507
544
@@ -530,32 +567,6 @@ data class GitHubPullEntry(
530
567
@SerializedName(" merge_commit_sha" ) val mergeCommitSha : String? ,
531
568
)
532
569
533
- /* *
534
- * Find a link to the original Pull request:
535
- * - to show the original PR instead of cherry-pick to users
536
- * (and the cherry-pick PR can be found in the comments, GitHub mentions all the links)
537
- * - to distinguish in case the diff is changed
538
- *
539
- * The link should be in format "Cherry-picked from <link>"
540
- */
541
- fun GitHubPullEntry.findCherryPickPullNumber (): Int? = body
542
- ?.lowercase()
543
- ?.split(" \n " )
544
- ?.map { it.trim() }
545
- ?.find { it.startsWithAny(" cherry-pick" , " cherrypick" , " cherry pick" ) }
546
- ?.let {
547
- val numberFromLink = it
548
- .substringAfter(" http" , " " )
549
- .substringAfter(" /pull/" , " " )
550
- .substringBefore(" " )
551
- .toIntOrNull()
552
- val numberFromId = it
553
- .substringAfter(" #" , " " )
554
- .substringBefore(" " )
555
- .toIntOrNull()
556
- numberFromLink ? : numberFromId
557
- }
558
-
559
570
// region ========================================== UTILS =========================================
560
571
fun pipeProcess (command : String ) = ProcessBuilder (command.split(" " ))
561
572
.redirectOutput(Redirect .PIPE )
@@ -578,7 +589,11 @@ fun Process.waitAndCheck() {
578
589
}
579
590
}
580
591
581
- fun Process.readText (): String = inputStream.bufferedReader().use { it.readText() }
592
+ fun Process.readText (): String = inputStream.bufferedReader().use {
593
+ it.readText().also {
594
+ waitAndCheck()
595
+ }
596
+ }
582
597
583
598
inline fun <reified T > requestJson (url : String ): T =
584
599
Gson ().fromJson(requestPlain(url), T ::class .java)
@@ -588,7 +603,7 @@ fun requestPlain(url: String): String = exponentialRetry {
588
603
val connection = URL (url).openConnection()
589
604
connection.setRequestProperty(" User-Agent" , " Compose-Multiplatform-Script" )
590
605
if (token != null ) {
591
- connection.setRequestProperty(" Authorization" , if (token.startsWith( " github_pat " )) token else " Bearer $token " )
606
+ connection.setRequestProperty(" Authorization" , " Bearer $token " )
592
607
}
593
608
connection.getInputStream().use {
594
609
it.bufferedReader().readText()
@@ -613,4 +628,9 @@ fun <T> exponentialRetry(block: () -> T): T {
613
628
614
629
fun String.startsWithAny (vararg prefixes : String ): Boolean = prefixes.any { startsWith(it) }
615
630
631
+ class Cache <K , V >(private val create : (K ) -> V ) {
632
+ private val map = mutableMapOf<K ,V >()
633
+ operator fun get (key : K ): V = map.getOrPut(key) { create(key) }
634
+ }
635
+
616
636
// endregion
0 commit comments