diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckRun.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckRun.java index 09c8a72d4..6d79ca93d 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckRun.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckRun.java @@ -35,6 +35,7 @@ import java.util.*; import java.util.logging.Logger; import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.*; class CheckRun { @@ -60,6 +61,7 @@ class CheckRun { private static final String emptyPrBodyMarker = ""; private static final String fullNameWarningMarker = ""; + private static final Pattern BACKPORT_PATTERN = Pattern.compile(""); private final Set newLabels; private Duration expiresIn; @@ -271,6 +273,116 @@ private void updateReadyForReview(PullRequestCheckIssueVisitor visitor, List(); + for (var patch : commit.parentDiffs().get(0).patches()) { + originalPatches.put(patch.toString(), patch); + } + var prPatches = new HashMap(); + for (var patch : pr.diff().patches()) { + prPatches.put(patch.toString(), patch); + } + + if (originalPatches.size() != prPatches.size()) { + if (hasCleanLabel) { + pr.removeLabel("clean"); + } + return false; + } + + var descriptions = new HashSet<>(originalPatches.keySet()); + descriptions.removeAll(prPatches.keySet()); + if (!descriptions.isEmpty()) { + if (hasCleanLabel) { + pr.removeLabel("clean"); + } + return false; + } + + for (var desc : originalPatches.keySet()) { + var original = originalPatches.get(desc).asTextualPatch(); + var backport = prPatches.get(desc).asTextualPatch(); + if (original.hunks().size() != backport.hunks().size()) { + if (hasCleanLabel) { + pr.removeLabel("clean"); + } + return false; + } + if (original.additions() != backport.additions()) { + if (hasCleanLabel) { + pr.removeLabel("clean"); + } + return false; + } + if (original.deletions() != backport.deletions()) { + if (hasCleanLabel) { + pr.removeLabel("clean"); + } + return false; + } + for (var i = 0; i < original.hunks().size(); i++) { + var originalHunk = original.hunks().get(i); + var backportHunk = backport.hunks().get(i); + + if (originalHunk.source().lines().size() != backportHunk.source().lines().size()) { + if (hasCleanLabel) { + pr.removeLabel("clean"); + } + return false; + } + var sourceLines = new HashSet<>(originalHunk.source().lines()); + sourceLines.removeAll(backportHunk.source().lines()); + if (!sourceLines.isEmpty()) { + if (hasCleanLabel) { + pr.removeLabel("clean"); + } + return false; + } + + if (originalHunk.target().lines().size() != backportHunk.target().lines().size()) { + if (hasCleanLabel) { + pr.removeLabel("clean"); + } + return false; + } + var targetLines = new HashSet<>(originalHunk.target().lines()); + targetLines.removeAll(backportHunk.target().lines()); + if (!targetLines.isEmpty()) { + if (hasCleanLabel) { + pr.removeLabel("clean"); + } + return false; + } + } + } + + if (!hasCleanLabel) { + pr.addLabel("clean"); + } + return true; + } + + private Optional backportedFrom() { + var botUser = pr.repository().forge().currentUser(); + var backportLines = pr.comments() + .stream() + .filter(c -> c.author().equals(botUser)) + .flatMap(c -> Stream.of(c.body().split("\n"))) + .map(l -> BACKPORT_PATTERN.matcher(l)) + .filter(Matcher::find) + .collect(Collectors.toList()); + return backportLines.isEmpty()? + Optional.empty() : Optional.of(new Hash(backportLines.get(0).group(1))); + } + private String getRole(String username) { var project = censusInstance.project(); var version = censusInstance.census().version().format(); @@ -846,6 +958,8 @@ private void checkStatus() { List additionalErrors = List.of(); Hash localHash; try { + // Do not pass eventual original commit even for backports since it will cause + // the reviewer check to be ignored. localHash = checkablePullRequest.commit(commitHash, censusInstance.namespace(), censusDomain, null, null); } catch (CommitFailure e) { additionalErrors = List.of(e.getMessage()); @@ -864,6 +978,11 @@ private void checkStatus() { } updateCheckBuilder(checkBuilder, visitor, additionalErrors); updateReadyForReview(visitor, additionalErrors); + var original = backportedFrom(); + var isCleanBackport = false; + if (original.isPresent()) { + isCleanBackport = updateClean(original.get()); + } var integrationBlockers = botSpecificIntegrationBlockers(); @@ -877,12 +996,18 @@ private void checkStatus() { updateReviewedMessages(comments, allReviews); } - var amendedHash = checkablePullRequest.amendManualReviewers(localHash, censusInstance.namespace()); + var amendedHash = checkablePullRequest.amendManualReviewers(localHash, censusInstance.namespace(), original.orElse(null)); var commit = localRepo.lookup(amendedHash).orElseThrow(); var commitMessage = String.join("\n", commit.message()); var readyForIntegration = visitor.messages().isEmpty() && additionalErrors.isEmpty() && integrationBlockers.isEmpty(); + if (isCleanBackport) { + // Reviews are not needed for clean backports + readyForIntegration = visitor.isReadyForReview() && + additionalErrors.isEmpty() && + integrationBlockers.isEmpty(); + } updateMergeReadyComment(readyForIntegration, commitMessage, comments, activeReviews, rebasePossible); if (readyForIntegration && rebasePossible) { diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckablePullRequest.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckablePullRequest.java index 1a98ad5a7..4c743903e 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckablePullRequest.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckablePullRequest.java @@ -139,10 +139,10 @@ Hash commit(Hash finalHead, Namespace namespace, String censusDomain, String spo return PullRequestUtils.createCommit(pr, localRepo, finalHead, author, committer, commitMessage); } - Hash amendManualReviewers(Hash commit, Namespace namespace) throws IOException { + Hash amendManualReviewers(Hash commit, Namespace namespace, Hash original) throws IOException { var activeReviews = filterActiveReviews(pr.reviews()); - var originalCommitMessage = commitMessage(activeReviews, namespace, false); - var amendedCommitMessage = commitMessage(activeReviews, namespace, true); + var originalCommitMessage = commitMessage(activeReviews, namespace, false, original); + var amendedCommitMessage = commitMessage(activeReviews, namespace, true, original); if (originalCommitMessage.equals(amendedCommitMessage)) { return commit; diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IntegrateCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IntegrateCommand.java index 038dd04b9..586f75b28 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IntegrateCommand.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IntegrateCommand.java @@ -163,7 +163,7 @@ public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInst // Rebase and push it! if (!localHash.equals(pr.targetHash())) { - var amendedHash = checkablePr.amendManualReviewers(localHash, censusInstance.namespace()); + var amendedHash = checkablePr.amendManualReviewers(localHash, censusInstance.namespace(), original); var finalRebaseMessage = rebaseMessage.toString(); if (!finalRebaseMessage.isBlank()) { reply.println(rebaseMessage.toString()); diff --git a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SponsorCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SponsorCommand.java index e6ce63a5f..6bef48fc1 100644 --- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SponsorCommand.java +++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SponsorCommand.java @@ -38,7 +38,7 @@ public class SponsorCommand implements CommandHandler { private final Logger log = Logger.getLogger("org.openjdk.skara.bots.pr"); - private static final Pattern BACKPORT_PATTERN = Pattern.compile("<-- backport ([0-9a-z]{40}) -->"); + private static final Pattern BACKPORT_PATTERN = Pattern.compile(""); @Override public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInstance, Path scratchPath, CommandInvocation command, List allComments, PrintWriter reply) { @@ -137,7 +137,7 @@ public void handle(PullRequestBot bot, PullRequest pr, CensusInstance censusInst } if (!localHash.equals(pr.targetHash())) { - var amendedHash = checkablePr.amendManualReviewers(localHash, censusInstance.namespace()); + var amendedHash = checkablePr.amendManualReviewers(localHash, censusInstance.namespace(), original); var finalRebaseMessage = rebaseMessage.toString(); if (!finalRebaseMessage.isBlank()) { reply.println(rebaseMessage.toString()); diff --git a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/BackportTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/BackportTests.java index 486086f62..3ad41be14 100644 --- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/BackportTests.java +++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/BackportTests.java @@ -89,8 +89,9 @@ void simple(TestInfo testInfo) throws IOException { // The bot should reply with a backport message TestBotRunner.runPeriodicItems(bot); - assertLastCommentContains(pr, "This backport pull request has now been updated with issue"); - assertLastCommentContains(pr, ""); + var backportComment = pr.comments().get(0).body(); + assertTrue(backportComment.contains("This backport pull request has now been updated with issue")); + assertTrue(backportComment.contains("")); assertEquals(issue1Number + ": An issue", pr.title()); // Approve PR and re-run bot @@ -125,10 +126,6 @@ void simple(TestInfo testInfo) throws IOException { var commit = localRepo.lookup(new Hash(hex)).orElseThrow(); var message = CommitMessageParsers.v1.parse(commit); - for (var c : pr.comments()) { - System.out.println(c.body()); - System.out.println("-------------------------------"); - } assertEquals(1, message.issues().size()); assertEquals("An issue", message.issues().get(0).description()); assertEquals(List.of("integrationreviewer3"), message.reviewers()); @@ -192,8 +189,10 @@ void withSummary(TestInfo testInfo) throws IOException { // The bot should reply with a backport message TestBotRunner.runPeriodicItems(bot); - assertLastCommentContains(pr, "This backport pull request has now been updated with issue and summary"); - assertLastCommentContains(pr, ""); + var comments = pr.comments(); + var backportComment = comments.get(0).body(); + assertTrue(backportComment.contains("This backport pull request has now been updated with issue")); + assertTrue(backportComment.contains("")); assertEquals(issue1Number + ": An issue", pr.title()); // Approve PR and re-run bot @@ -228,10 +227,6 @@ void withSummary(TestInfo testInfo) throws IOException { var commit = localRepo.lookup(new Hash(hex)).orElseThrow(); var message = CommitMessageParsers.v1.parse(commit); - for (var c : pr.comments()) { - System.out.println(c.body()); - System.out.println("-------------------------------"); - } assertEquals(1, message.issues().size()); assertEquals("An issue", message.issues().get(0).description()); assertEquals(List.of("integrationreviewer3"), message.reviewers()); @@ -298,8 +293,9 @@ void withMultipleIssues(TestInfo testInfo) throws IOException { // The bot should reply with a backport message TestBotRunner.runPeriodicItems(bot); - assertLastCommentContains(pr, "This backport pull request has now been updated with issues and summary"); - assertLastCommentContains(pr, ""); + var backportComment = pr.comments().get(0).body(); + assertTrue(backportComment.contains("This backport pull request has now been updated with issue")); + assertTrue(backportComment.contains("")); assertEquals(issue1Number + ": An issue", pr.title()); // Approve PR and re-run bot @@ -334,10 +330,6 @@ void withMultipleIssues(TestInfo testInfo) throws IOException { var commit = localRepo.lookup(new Hash(hex)).orElseThrow(); var message = CommitMessageParsers.v1.parse(commit); - for (var c : pr.comments()) { - System.out.println(c.body()); - System.out.println("-------------------------------"); - } assertEquals(2, message.issues().size()); assertEquals("An issue", message.issues().get(0).description()); assertEquals("Another issue", message.issues().get(1).description()); @@ -390,4 +382,484 @@ void nonExitingCommit(TestInfo testInfo) throws IOException { assertEquals(1, pr.comments().size()); } } + + @Test + void cleanBackport(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory(false); + var pushedFolder = new TemporaryDirectory(false)) { + + var author = credentials.getHostedRepository(); + var integrator = credentials.getHostedRepository(); + var reviewer = credentials.getHostedRepository(); + var issues = credentials.getIssueProject(); + var censusBuilder = credentials.getCensusBuilder() + .addCommitter(author.forge().currentUser().id()) + .addReviewer(integrator.forge().currentUser().id()) + .addReviewer(reviewer.forge().currentUser().id()); + var bot = PullRequestBot.newBuilder() + .repo(integrator) + .censusRepo(censusBuilder.build()) + .issueProject(issues) + .build(); + + // Populate the projects repository + var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType()); + var masterHash = localRepo.resolve("master").orElseThrow(); + localRepo.push(masterHash, author.url(), "master", true); + + var releaseBranch = localRepo.branch(masterHash, "release"); + localRepo.checkout(releaseBranch); + var newFile = localRepo.root().resolve("a_new_file.txt"); + Files.writeString(newFile, "hello"); + localRepo.add(newFile); + var issue1 = credentials.createIssue(issues, "An issue"); + var issue1Number = issue1.id().split("-")[1]; + var originalMessage = issue1Number + ": An issue\n" + + "\n" + + "Reviewed-by: integrationreviewer2"; + var releaseHash = localRepo.commit(originalMessage, "integrationcommitter1", "integrationcommitter1@openjdk.java.net"); + localRepo.push(releaseHash, author.url(), "refs/heads/release", true); + + // "backport" the new file to the master branch + localRepo.checkout(localRepo.defaultBranch()); + var editBranch = localRepo.branch(masterHash, "edit"); + localRepo.checkout(editBranch); + var newFile2 = localRepo.root().resolve("a_new_file.txt"); + Files.writeString(newFile2, "hello"); + localRepo.add(newFile2); + var editHash = localRepo.commit("Backport", "duke", "duke@openjdk.java.net"); + localRepo.push(editHash, author.url(), "refs/heads/edit", true); + var pr = credentials.createPullRequest(author, "master", "edit", "Backport " + releaseHash.hex()); + + // The bot should reply with a backport message + TestBotRunner.runPeriodicItems(bot); + var comments = pr.comments(); + var backportComment = comments.get(0).body(); + assertTrue(backportComment.contains("This backport pull request has now been updated with issue")); + assertTrue(backportComment.contains("")); + assertEquals(issue1Number + ": An issue", pr.title()); + + // The bot should have added the "clean" label + assertTrue(pr.labels().contains("clean")); + } + } + + @Test + void fuzzyCleanBackport(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory(false); + var pushedFolder = new TemporaryDirectory(false)) { + + var author = credentials.getHostedRepository(); + var integrator = credentials.getHostedRepository(); + var reviewer = credentials.getHostedRepository(); + var issues = credentials.getIssueProject(); + var censusBuilder = credentials.getCensusBuilder() + .addCommitter(author.forge().currentUser().id()) + .addReviewer(integrator.forge().currentUser().id()) + .addReviewer(reviewer.forge().currentUser().id()); + var bot = PullRequestBot.newBuilder() + .repo(integrator) + .censusRepo(censusBuilder.build()) + .issueProject(issues) + .build(); + + // Populate the projects repository + var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType()); + + var newFile = localRepo.root().resolve("a_new_file.txt"); + Files.writeString(newFile, "a\nb\nc\nd\n"); + localRepo.add(newFile); + var issue1 = credentials.createIssue(issues, "An issue"); + var issue1Number = issue1.id().split("-")[1]; + var originalMessage = issue1Number + ": An issue\n" + + "\n" + + "Reviewed-by: integrationreviewer2"; + var masterHash = localRepo.commit(originalMessage, "integrationcommitter1", "integrationcommitter1@openjdk.java.net"); + + localRepo.push(masterHash, author.url(), "master", true); + + var releaseBranch = localRepo.branch(masterHash, "release"); + localRepo.checkout(releaseBranch); + Files.writeString(newFile, "a\nb\nc\nd\ne"); + localRepo.add(newFile); + var issue2 = credentials.createIssue(issues, "Another issue"); + var issue2Number = issue2.id().split("-")[1]; + var upstreamMessage = issue2Number + ": Another issue\n" + + "\n" + + "Reviewed-by: integrationreviewer2"; + var upstreamHash = localRepo.commit(upstreamMessage, "integrationcommitter1", "integrationcommitter1@openjdk.java.net"); + localRepo.push(upstreamHash, author.url(), "refs/heads/release", true); + + // "backport" the new file to the master branch + localRepo.checkout(localRepo.defaultBranch()); + var editBranch = localRepo.branch(masterHash, "edit"); + localRepo.checkout(editBranch); + Files.writeString(newFile, "a\nb\nc\ne\nd\n"); + localRepo.add(newFile); + var editHash = localRepo.commit("Backport", "duke", "duke@openjdk.java.net"); + localRepo.push(editHash, author.url(), "refs/heads/edit", true); + var pr = credentials.createPullRequest(author, "master", "edit", "Backport " + upstreamHash.hex()); + + // The bot should reply with a backport message + TestBotRunner.runPeriodicItems(bot); + var comments = pr.comments(); + var backportComment = comments.get(0).body(); + assertTrue(backportComment.contains("This backport pull request has now been updated with issue")); + assertTrue(backportComment.contains("")); + assertEquals(issue2Number + ": Another issue", pr.title()); + + // The bot should have added the "clean" label + assertTrue(pr.labels().contains("clean")); + } + } + + @Test + void notCleanBackport(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory(false); + var pushedFolder = new TemporaryDirectory(false)) { + + var author = credentials.getHostedRepository(); + var integrator = credentials.getHostedRepository(); + var reviewer = credentials.getHostedRepository(); + var issues = credentials.getIssueProject(); + var censusBuilder = credentials.getCensusBuilder() + .addCommitter(author.forge().currentUser().id()) + .addReviewer(integrator.forge().currentUser().id()) + .addReviewer(reviewer.forge().currentUser().id()); + var bot = PullRequestBot.newBuilder() + .repo(integrator) + .censusRepo(censusBuilder.build()) + .issueProject(issues) + .build(); + + // Populate the projects repository + var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType()); + + var newFile = localRepo.root().resolve("a_new_file.txt"); + Files.writeString(newFile, "a\nb\nc\nd"); + localRepo.add(newFile); + var issue1 = credentials.createIssue(issues, "An issue"); + var issue1Number = issue1.id().split("-")[1]; + var originalMessage = issue1Number + ": An issue\n" + + "\n" + + "Reviewed-by: integrationreviewer2"; + var masterHash = localRepo.commit(originalMessage, "integrationcommitter1", "integrationcommitter1@openjdk.java.net"); + + localRepo.push(masterHash, author.url(), "master", true); + + var releaseBranch = localRepo.branch(masterHash, "release"); + localRepo.checkout(releaseBranch); + Files.writeString(newFile, "a\nb\nc\nd\ne"); + localRepo.add(newFile); + var issue2 = credentials.createIssue(issues, "Another issue"); + var issue2Number = issue2.id().split("-")[1]; + var upstreamMessage = issue2Number + ": Another issue\n" + + "\n" + + "Reviewed-by: integrationreviewer2"; + var upstreamHash = localRepo.commit(upstreamMessage, "integrationcommitter1", "integrationcommitter1@openjdk.java.net"); + localRepo.push(upstreamHash, author.url(), "refs/heads/release", true); + + // "backport" the new file to the master branch + localRepo.checkout(localRepo.defaultBranch()); + var editBranch = localRepo.branch(masterHash, "edit"); + localRepo.checkout(editBranch); + Files.writeString(newFile, "a\nb\nc\nd\nd"); + localRepo.add(newFile); + var editHash = localRepo.commit("Backport", "duke", "duke@openjdk.java.net"); + localRepo.push(editHash, author.url(), "refs/heads/edit", true); + var pr = credentials.createPullRequest(author, "master", "edit", "Backport " + upstreamHash.hex()); + + // The bot should reply with a backport message + TestBotRunner.runPeriodicItems(bot); + var comments = pr.comments(); + var backportComment = comments.get(0).body(); + assertTrue(backportComment.contains("This backport pull request has now been updated with issue")); + assertTrue(backportComment.contains("")); + assertEquals(issue2Number + ": Another issue", pr.title()); + + // The bot should not have added the "clean" label + assertFalse(pr.labels().contains("clean")); + } + } + + @Test + void notCleanBackportAdditionalFile(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory(false); + var pushedFolder = new TemporaryDirectory(false)) { + + var author = credentials.getHostedRepository(); + var integrator = credentials.getHostedRepository(); + var reviewer = credentials.getHostedRepository(); + var issues = credentials.getIssueProject(); + var censusBuilder = credentials.getCensusBuilder() + .addCommitter(author.forge().currentUser().id()) + .addReviewer(integrator.forge().currentUser().id()) + .addReviewer(reviewer.forge().currentUser().id()); + var bot = PullRequestBot.newBuilder() + .repo(integrator) + .censusRepo(censusBuilder.build()) + .issueProject(issues) + .build(); + + // Populate the projects repository + var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType()); + + var newFile = localRepo.root().resolve("a_new_file.txt"); + Files.writeString(newFile, "a\nb\nc\nd"); + localRepo.add(newFile); + var issue1 = credentials.createIssue(issues, "An issue"); + var issue1Number = issue1.id().split("-")[1]; + var originalMessage = issue1Number + ": An issue\n" + + "\n" + + "Reviewed-by: integrationreviewer2"; + var masterHash = localRepo.commit(originalMessage, "integrationcommitter1", "integrationcommitter1@openjdk.java.net"); + + localRepo.push(masterHash, author.url(), "master", true); + + var releaseBranch = localRepo.branch(masterHash, "release"); + localRepo.checkout(releaseBranch); + Files.writeString(newFile, "a\nb\nc\nd\ne"); + localRepo.add(newFile); + var issue2 = credentials.createIssue(issues, "Another issue"); + var issue2Number = issue2.id().split("-")[1]; + var upstreamMessage = issue2Number + ": Another issue\n" + + "\n" + + "Reviewed-by: integrationreviewer2"; + var upstreamHash = localRepo.commit(upstreamMessage, "integrationcommitter1", "integrationcommitter1@openjdk.java.net"); + localRepo.push(upstreamHash, author.url(), "refs/heads/release", true); + + // "backport" the new file to the master branch + localRepo.checkout(localRepo.defaultBranch()); + var editBranch = localRepo.branch(masterHash, "edit"); + localRepo.checkout(editBranch); + Files.writeString(newFile, "a\nb\nc\nd\ne"); + localRepo.add(newFile); + var anotherFile = localRepo.root().resolve("another_file.txt"); + Files.writeString(anotherFile, "f\ng\nh\ni"); + localRepo.add(anotherFile); + var editHash = localRepo.commit("Backport", "duke", "duke@openjdk.java.net"); + localRepo.push(editHash, author.url(), "refs/heads/edit", true); + var pr = credentials.createPullRequest(author, "master", "edit", "Backport " + upstreamHash.hex()); + + // The bot should reply with a backport message + TestBotRunner.runPeriodicItems(bot); + var comments = pr.comments(); + var backportComment = comments.get(0).body(); + assertTrue(backportComment.contains("This backport pull request has now been updated with issue")); + assertTrue(backportComment.contains("")); + assertEquals(issue2Number + ": Another issue", pr.title()); + + // The bot should not have added the "clean" label + assertFalse(pr.labels().contains("clean")); + } + } + + @Test + void cleanBackportFromCommitterCanBeIntegrated(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory(); + var pushedFolder = new TemporaryDirectory()) { + + var author = credentials.getHostedRepository(); + var integrator = credentials.getHostedRepository(); + var reviewer = credentials.getHostedRepository(); + var issues = credentials.getIssueProject(); + var censusBuilder = credentials.getCensusBuilder() + .addCommitter(author.forge().currentUser().id()) + .addReviewer(integrator.forge().currentUser().id()) + .addReviewer(reviewer.forge().currentUser().id()); + var bot = PullRequestBot.newBuilder() + .repo(integrator) + .censusRepo(censusBuilder.build()) + .issueProject(issues) + .build(); + + // Populate the projects repository + var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType()); + var masterHash = localRepo.resolve("master").orElseThrow(); + localRepo.push(masterHash, author.url(), "master", true); + + var releaseBranch = localRepo.branch(masterHash, "release"); + localRepo.checkout(releaseBranch); + var newFile = localRepo.root().resolve("a_new_file.txt"); + Files.writeString(newFile, "hello"); + localRepo.add(newFile); + var issue1 = credentials.createIssue(issues, "An issue"); + var issue1Number = issue1.id().split("-")[1]; + var originalMessage = issue1Number + ": An issue\n" + + "\n" + + "Reviewed-by: integrationreviewer2"; + var releaseHash = localRepo.commit(originalMessage, "integrationcommitter1", "integrationcommitter1@openjdk.java.net"); + localRepo.push(releaseHash, author.url(), "refs/heads/release", true); + + // "backport" the new file to the master branch + localRepo.checkout(localRepo.defaultBranch()); + var editBranch = localRepo.branch(masterHash, "edit"); + localRepo.checkout(editBranch); + var newFile2 = localRepo.root().resolve("a_new_file.txt"); + Files.writeString(newFile2, "hello"); + localRepo.add(newFile2); + var editHash = localRepo.commit("Backport", "duke", "duke@openjdk.java.net"); + localRepo.push(editHash, author.url(), "refs/heads/edit", true); + var pr = credentials.createPullRequest(author, "master", "edit", "Backport " + releaseHash.hex()); + + // The bot should reply with a backport message and that the PR is ready + TestBotRunner.runPeriodicItems(bot); + var backportComment = pr.comments().get(0).body(); + assertTrue(backportComment.contains("This backport pull request has now been updated with issue")); + assertTrue(backportComment.contains("")); + assertEquals(issue1Number + ": An issue", pr.title()); + assertLastCommentContains(pr, "This change now passes all *automated* pre-integration checks"); + assertTrue(pr.labels().contains("ready")); + assertTrue(pr.labels().contains("rfr")); + assertTrue(pr.labels().contains("clean")); + + // Integrate + var prAsCommitter = author.pullRequest(pr.id()); + pr.addComment("/integrate"); + TestBotRunner.runPeriodicItems(bot); + + // Find the commit + assertLastCommentContains(pr, "Pushed as commit"); + + String hex = null; + var comment = pr.comments().get(pr.comments().size() - 1); + var lines = comment.body().split("\n"); + var pattern = Pattern.compile(".* Pushed as commit ([0-9a-z]{40}).*"); + for (var line : lines) { + var m = pattern.matcher(line); + if (m.matches()) { + hex = m.group(1); + break; + } + } + assertNotNull(hex); + assertEquals(40, hex.length()); + localRepo.checkout(localRepo.defaultBranch()); + localRepo.pull(author.url().toString(), "master", false); + var commit = localRepo.lookup(new Hash(hex)).orElseThrow(); + + var message = CommitMessageParsers.v1.parse(commit); + assertEquals(1, message.issues().size()); + assertEquals("An issue", message.issues().get(0).description()); + assertEquals(List.of(), message.reviewers()); + assertEquals(Optional.of(releaseHash), message.original()); + assertEquals(List.of(), message.contributors()); + assertEquals(List.of(), message.summaries()); + assertEquals(List.of(), message.additional()); + } + } + + @Test + void cleanBackportFromAuthorCanBeIntegrated(TestInfo testInfo) throws IOException { + try (var credentials = new HostCredentials(testInfo); + var tempFolder = new TemporaryDirectory(); + var pushedFolder = new TemporaryDirectory()) { + + var author = credentials.getHostedRepository(); + var integrator = credentials.getHostedRepository(); + var reviewer = credentials.getHostedRepository(); + var issues = credentials.getIssueProject(); + var censusBuilder = credentials.getCensusBuilder() + .addAuthor(author.forge().currentUser().id()) + .addReviewer(integrator.forge().currentUser().id()) + .addReviewer(reviewer.forge().currentUser().id()); + var bot = PullRequestBot.newBuilder() + .repo(integrator) + .censusRepo(censusBuilder.build()) + .issueProject(issues) + .build(); + + // Populate the projects repository + var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType()); + var masterHash = localRepo.resolve("master").orElseThrow(); + localRepo.push(masterHash, author.url(), "master", true); + + var releaseBranch = localRepo.branch(masterHash, "release"); + localRepo.checkout(releaseBranch); + var newFile = localRepo.root().resolve("a_new_file.txt"); + Files.writeString(newFile, "hello"); + localRepo.add(newFile); + var issue1 = credentials.createIssue(issues, "An issue"); + var issue1Number = issue1.id().split("-")[1]; + var originalMessage = issue1Number + ": An issue\n" + + "\n" + + "Reviewed-by: integrationreviewer2"; + var releaseHash = localRepo.commit(originalMessage, "integrationcommitter1", "integrationcommitter1@openjdk.java.net"); + localRepo.push(releaseHash, author.url(), "refs/heads/release", true); + + // "backport" the new file to the master branch + localRepo.checkout(localRepo.defaultBranch()); + var editBranch = localRepo.branch(masterHash, "edit"); + localRepo.checkout(editBranch); + var newFile2 = localRepo.root().resolve("a_new_file.txt"); + Files.writeString(newFile2, "hello"); + localRepo.add(newFile2); + var editHash = localRepo.commit("Backport", "duke", "duke@openjdk.java.net"); + localRepo.push(editHash, author.url(), "refs/heads/edit", true); + var pr = credentials.createPullRequest(author, "master", "edit", "Backport " + releaseHash.hex()); + + // The bot should reply with a backport message and that the PR is ready + TestBotRunner.runPeriodicItems(bot); + var backportComment = pr.comments().get(0).body(); + assertTrue(backportComment.contains("This backport pull request has now been updated with issue")); + assertTrue(backportComment.contains("")); + assertEquals(issue1Number + ": An issue", pr.title()); + assertLastCommentContains(pr, "This change now passes all *automated* pre-integration checks"); + assertTrue(pr.labels().contains("ready")); + assertTrue(pr.labels().contains("rfr")); + assertTrue(pr.labels().contains("clean")); + assertFalse(pr.labels().contains("sponsor")); + + // Integrate + var prAsAuthor = author.pullRequest(pr.id()); + prAsAuthor.addComment("/integrate"); + TestBotRunner.runPeriodicItems(bot); + + // The bot should reply with a sponsor message + assertTrue(pr.labels().contains("sponsor")); + + // Sponsor the commit + var prAsReviewer = reviewer.pullRequest(pr.id()); + prAsReviewer.addComment("/sponsor"); + TestBotRunner.runPeriodicItems(bot); + + // Find the commit + for (var comment : pr.comments()) { + System.out.println(comment.body()); + } + assertLastCommentContains(pr, "Pushed as commit"); + + String hex = null; + var comment = pr.comments().get(pr.comments().size() - 1); + var lines = comment.body().split("\n"); + var pattern = Pattern.compile(".* Pushed as commit ([0-9a-z]{40}).*"); + for (var line : lines) { + var m = pattern.matcher(line); + if (m.matches()) { + hex = m.group(1); + break; + } + } + assertNotNull(hex); + assertEquals(40, hex.length()); + localRepo.checkout(localRepo.defaultBranch()); + localRepo.pull(author.url().toString(), "master", false); + var commit = localRepo.lookup(new Hash(hex)).orElseThrow(); + + var message = CommitMessageParsers.v1.parse(commit); + assertNotEquals(commit.author(), commit.committer()); + assertEquals(1, message.issues().size()); + assertEquals("An issue", message.issues().get(0).description()); + assertEquals(List.of(), message.reviewers()); + assertEquals(Optional.of(releaseHash), message.original()); + assertEquals(List.of(), message.contributors()); + assertEquals(List.of(), message.summaries()); + assertEquals(List.of(), message.additional()); + } + } }