|
| 1 | +/* |
| 2 | + * Licensed to Elasticsearch B.V. under one or more contributor |
| 3 | + * license agreements. See the NOTICE file distributed with |
| 4 | + * this work for additional information regarding copyright |
| 5 | + * ownership. Elasticsearch B.V. licenses this file to you under |
| 6 | + * the Apache License, Version 2.0 (the "License"); you may |
| 7 | + * not use this file except in compliance with the License. |
| 8 | + * You may obtain a copy of the License at |
| 9 | + * |
| 10 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | + * |
| 12 | + * Unless required by applicable law or agreed to in writing, |
| 13 | + * software distributed under the License is distributed on an |
| 14 | + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| 15 | + * KIND, either express or implied. See the License for the |
| 16 | + * specific language governing permissions and limitations |
| 17 | + * under the License. |
| 18 | + */ |
| 19 | + |
| 20 | +import java.io.IOException; |
| 21 | +import java.nio.charset.StandardCharsets; |
| 22 | +import java.nio.file.Files; |
| 23 | +import java.nio.file.Path; |
| 24 | +import java.nio.file.Paths; |
| 25 | +import java.time.LocalDate; |
| 26 | +import java.time.format.DateTimeFormatter; |
| 27 | +import java.util.ArrayList; |
| 28 | +import java.util.Comparator; |
| 29 | +import java.util.List; |
| 30 | +import java.util.Locale; |
| 31 | +import java.util.OptionalInt; |
| 32 | +import java.util.function.Predicate; |
| 33 | +import java.util.regex.Matcher; |
| 34 | +import java.util.regex.Pattern; |
| 35 | + |
| 36 | +public class ReleaseChangelog { |
| 37 | + |
| 38 | + public static void main(String[] args) throws IOException { |
| 39 | + if (args.length != 3) { |
| 40 | + System.out.println( |
| 41 | + "Expected exactly three arguments: <ChangelogFile> <ReleaseNotesPath> <VersionToRelease>"); |
| 42 | + System.exit(-1); |
| 43 | + } |
| 44 | + Path nextChangelogFile = Paths.get(args[0]); |
| 45 | + Path releaseNotesDir = Paths.get(args[1]); |
| 46 | + Path releaseNotesFile = releaseNotesDir.resolve("index.md"); |
| 47 | + Path deprecationsFile = releaseNotesDir.resolve("deprecations.md"); |
| 48 | + Path breakingChangesFile = releaseNotesDir.resolve("breaking-changes.md"); |
| 49 | + VersionNumber version = VersionNumber.parse(args[2].trim()); |
| 50 | + |
| 51 | + Lines nextChangelogLines = new Lines( |
| 52 | + Files.readAllLines(nextChangelogFile, StandardCharsets.UTF_8)); |
| 53 | + Lines fixes = nextChangelogLines.cutLinesBetween("<!--FIXES-START-->", "<!--FIXES-END-->"); |
| 54 | + Lines enhancements = nextChangelogLines.cutLinesBetween("<!--ENHANCEMENTS-START-->", |
| 55 | + "<!--ENHANCEMENTS-END-->"); |
| 56 | + Lines deprecations = nextChangelogLines.cutLinesBetween("<!--DEPRECATIONS-START-->", |
| 57 | + "<!--DEPRECATIONS-END-->"); |
| 58 | + Lines breakingChanges = nextChangelogLines.cutLinesBetween("<!--BREAKING-CHANGES-START-->", |
| 59 | + "<!--BREAKING-CHANGES-END-->"); |
| 60 | + |
| 61 | + Lines dependenciesNotes = nextChangelogLines.cutLinesBetween("<!--DEPENDENCIES-NOTES-START-->", |
| 62 | + "<!--DEPENDENCIES-NOTES-END-->"); |
| 63 | + |
| 64 | + var formatter = DateTimeFormatter.ofPattern("LLLL d, yyyy", Locale.ENGLISH); |
| 65 | + String releaseDateLine = "**Release date:** " + formatter.format(LocalDate.now()); |
| 66 | + |
| 67 | + Lines allReleaseNotes = new Lines(Files.readAllLines(releaseNotesFile, StandardCharsets.UTF_8)); |
| 68 | + int insertBeforeLine = findHeadingOfPreviousVersion(allReleaseNotes, version); |
| 69 | + if (insertBeforeLine < 0) { |
| 70 | + insertBeforeLine = allReleaseNotes.lineCount(); |
| 71 | + } |
| 72 | + allReleaseNotes.insert( |
| 73 | + generateReleaseNotes(version, releaseDateLine, enhancements, fixes, breakingChanges, |
| 74 | + dependenciesNotes), |
| 75 | + insertBeforeLine); |
| 76 | + |
| 77 | + if (!deprecations.isEmpty()) { |
| 78 | + Lines allDeprecations = new Lines( |
| 79 | + Files.readAllLines(deprecationsFile, StandardCharsets.UTF_8)); |
| 80 | + int insertDepsBeforeLine = findHeadingOfPreviousVersion(allDeprecations, version); |
| 81 | + if (insertDepsBeforeLine < 0) { |
| 82 | + // in case no previous version was listed |
| 83 | + insertDepsBeforeLine = allDeprecations.lineCount(); |
| 84 | + } |
| 85 | + allDeprecations.insert(generateDeprecations(version, releaseDateLine, deprecations), |
| 86 | + insertDepsBeforeLine); |
| 87 | + Files.writeString(deprecationsFile, allDeprecations + "\n", StandardCharsets.UTF_8); |
| 88 | + } |
| 89 | + if (!breakingChanges.isEmpty()) { |
| 90 | + Lines allBreakingChanges = new Lines( |
| 91 | + Files.readAllLines(breakingChangesFile, StandardCharsets.UTF_8)); |
| 92 | + int insertBcBeforeLine = findHeadingOfPreviousVersion(allBreakingChanges, version); |
| 93 | + if (insertBcBeforeLine < 0) { |
| 94 | + // in case no previous version was listed |
| 95 | + insertBcBeforeLine = allBreakingChanges.lineCount(); |
| 96 | + } |
| 97 | + allBreakingChanges.insert(generateBreakingChanges(version, releaseDateLine, breakingChanges), |
| 98 | + insertBcBeforeLine); |
| 99 | + Files.writeString(breakingChangesFile, allBreakingChanges + "\n", StandardCharsets.UTF_8); |
| 100 | + } |
| 101 | + Files.writeString(releaseNotesFile, allReleaseNotes + "\n", StandardCharsets.UTF_8); |
| 102 | + Files.writeString(nextChangelogFile, nextChangelogLines + "\n", StandardCharsets.UTF_8); |
| 103 | + } |
| 104 | + |
| 105 | + private static Lines generateReleaseNotes(VersionNumber version, String releaseDateLine, |
| 106 | + Lines enhancements, Lines fixes, Lines breaking, Lines dependenciesNotes) { |
| 107 | + Lines result = new Lines() |
| 108 | + .append("## " + version.dotStr() + " [edot-java-" + version.dashStr() + "-release-notes]") |
| 109 | + .append(releaseDateLine); |
| 110 | + if (!enhancements.isEmpty()) { |
| 111 | + result |
| 112 | + .append("") |
| 113 | + .append("### Features and enhancements [edot-java-" + version.dashStr() |
| 114 | + + "-features-enhancements]") |
| 115 | + .append(enhancements); |
| 116 | + } |
| 117 | + if (!fixes.isEmpty()) { |
| 118 | + result |
| 119 | + .append("") |
| 120 | + .append("### Fixes [edot-java-" + version.dashStr() + "-fixes]") |
| 121 | + .append(fixes); |
| 122 | + } |
| 123 | + if (!breaking.isEmpty()) { |
| 124 | + result |
| 125 | + .append("") |
| 126 | + .append("### Breaking changes [edot-java-" + version.dashStr() + "-fixes]") |
| 127 | + .append(breaking); |
| 128 | + } |
| 129 | + if (!dependenciesNotes.isEmpty()) { |
| 130 | + result |
| 131 | + .append("") |
| 132 | + .append(dependenciesNotes); |
| 133 | + } |
| 134 | + result.append(""); |
| 135 | + return result; |
| 136 | + } |
| 137 | + |
| 138 | + private static Lines generateDeprecations(VersionNumber version, String releaseDateLine, |
| 139 | + Lines deprecations) { |
| 140 | + return new Lines() |
| 141 | + .append("## " + version.dotStr() + " [edot-java-" + version.dashStr() + "-deprecations]") |
| 142 | + .append(releaseDateLine) |
| 143 | + .append("") |
| 144 | + .append(deprecations) |
| 145 | + .append(""); |
| 146 | + } |
| 147 | + |
| 148 | + private static Lines generateBreakingChanges(VersionNumber version, String releaseDateLine, |
| 149 | + Lines breakingChanges) { |
| 150 | + return new Lines() |
| 151 | + .append("## " + version.dotStr() + " [" + version.dotStr() + "]") |
| 152 | + .append("") |
| 153 | + .append(releaseDateLine) |
| 154 | + .append("") |
| 155 | + .append(breakingChanges) |
| 156 | + .append(""); |
| 157 | + } |
| 158 | + |
| 159 | + static int findHeadingOfPreviousVersion(Lines lines, VersionNumber version) { |
| 160 | + Pattern headingPattern = Pattern.compile("## (\\d+\\.\\d+\\.\\d+) .*"); |
| 161 | + Comparator<VersionNumber> comp = VersionNumber.comparator(); |
| 162 | + int currentBestLineNo = -1; |
| 163 | + VersionNumber currentBestVersion = null; |
| 164 | + for (int i = 0; i < lines.lineCount(); i++) { |
| 165 | + Matcher matcher = headingPattern.matcher(lines.getLine(i)); |
| 166 | + if (matcher.matches()) { |
| 167 | + VersionNumber headingForVersion = VersionNumber.parse(matcher.group(1)); |
| 168 | + if (comp.compare(headingForVersion, version) < 0 |
| 169 | + && (currentBestVersion == null |
| 170 | + || comp.compare(headingForVersion, currentBestVersion) > 0)) { |
| 171 | + currentBestLineNo = i; |
| 172 | + currentBestVersion = headingForVersion; |
| 173 | + } |
| 174 | + } |
| 175 | + } |
| 176 | + return currentBestLineNo; |
| 177 | + } |
| 178 | + |
| 179 | + record VersionNumber(int major, int minor, int patch) { |
| 180 | + public static VersionNumber parse (String versionString){ |
| 181 | + if (!versionString.matches("\\d+\\.\\d+\\.\\d+")) { |
| 182 | + throw new IllegalArgumentException( |
| 183 | + "Version must be in the format x.x.x but was not: " + versionString); |
| 184 | + } |
| 185 | + String[] parts = versionString.split("\\."); |
| 186 | + return new VersionNumber(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]), |
| 187 | + Integer.parseInt(parts[2])); |
| 188 | + } |
| 189 | + |
| 190 | + static Comparator<VersionNumber> comparator () { |
| 191 | + return Comparator |
| 192 | + .comparing(VersionNumber::major) |
| 193 | + .thenComparing(VersionNumber::minor) |
| 194 | + .thenComparing(VersionNumber::patch); |
| 195 | + } |
| 196 | + |
| 197 | + String dashStr () { |
| 198 | + return major + "-" + minor + "-" + patch; |
| 199 | + } |
| 200 | + |
| 201 | + String dotStr () { |
| 202 | + return major + "." + minor + "." + patch; |
| 203 | + } |
| 204 | + |
| 205 | + } |
| 206 | + |
| 207 | + static class Lines { |
| 208 | + |
| 209 | + private final List<String> lines; |
| 210 | + |
| 211 | + public Lines() { |
| 212 | + this.lines = new ArrayList<>(); |
| 213 | + } |
| 214 | + |
| 215 | + public Lines(List<String> lines) { |
| 216 | + this.lines = new ArrayList<>(lines); |
| 217 | + } |
| 218 | + |
| 219 | + int lineCount() { |
| 220 | + return lines.size(); |
| 221 | + } |
| 222 | + |
| 223 | + boolean isEmpty() { |
| 224 | + return lines.isEmpty(); |
| 225 | + } |
| 226 | + |
| 227 | + Lines cutLinesBetween(String startLine, String endLine) { |
| 228 | + int start = findLine(l -> l.trim().equals(startLine), 0) |
| 229 | + .orElseThrow( |
| 230 | + () -> new IllegalStateException("Expected line '" + startLine + "' to exist")); |
| 231 | + int end = findLine(l -> l.trim().equals(endLine), start + 1) |
| 232 | + .orElseThrow(() -> new IllegalStateException( |
| 233 | + "Expected line '" + endLine + "' to exist after '" + startLine + "'")); |
| 234 | + Lines result = cut(start + 1, end).trim(); |
| 235 | + |
| 236 | + lines.add(start + 1, ""); |
| 237 | + |
| 238 | + return result; |
| 239 | + } |
| 240 | + |
| 241 | + OptionalInt findLine(Predicate<String> condition, int startAt) { |
| 242 | + for (int i = startAt; i < lines.size(); i++) { |
| 243 | + if (condition.test(lines.get(i))) { |
| 244 | + return OptionalInt.of(i); |
| 245 | + } |
| 246 | + } |
| 247 | + return OptionalInt.empty(); |
| 248 | + } |
| 249 | + |
| 250 | + Lines cut(int startInclusive, int endExclusive) { |
| 251 | + List<String> cutLines = new ArrayList<>(); |
| 252 | + for (int i = startInclusive; i < endExclusive; i++) { |
| 253 | + cutLines.add(lines.remove(startInclusive)); |
| 254 | + } |
| 255 | + return new Lines(cutLines); |
| 256 | + } |
| 257 | + |
| 258 | + void insert(Lines other, int insertAt) { |
| 259 | + this.lines.addAll(insertAt, other.lines); |
| 260 | + } |
| 261 | + |
| 262 | + Lines append(String line) { |
| 263 | + lines.add(line); |
| 264 | + return this; |
| 265 | + } |
| 266 | + |
| 267 | + Lines append(Lines toAppend) { |
| 268 | + lines.addAll(toAppend.lines); |
| 269 | + return this; |
| 270 | + } |
| 271 | + |
| 272 | + /** |
| 273 | + * Trims lines consisting of only blanks at the top and bottom |
| 274 | + */ |
| 275 | + Lines trim() { |
| 276 | + while (!lines.isEmpty() && lines.get(0).matches("\\s*")) { |
| 277 | + lines.remove(0); |
| 278 | + } |
| 279 | + while (!lines.isEmpty() && lines.get(lines.size() - 1).matches("\\s*")) { |
| 280 | + lines.remove(lines.size() - 1); |
| 281 | + } |
| 282 | + return this; |
| 283 | + } |
| 284 | + |
| 285 | + @Override |
| 286 | + public String toString() { |
| 287 | + return String.join("\n", lines); |
| 288 | + } |
| 289 | + |
| 290 | + public String getLine(int number) { |
| 291 | + return lines.get(number); |
| 292 | + } |
| 293 | + } |
| 294 | + |
| 295 | +} |
0 commit comments