Repository: openjdk/skara
Branch: master
Commit: 2a09ac33bf92
Files: 867
Total size: 6.4 MB
Directory structure:
gitextract_jzipxa8x/
├── .dockerignore
├── .gitattributes
├── .github/
│ ├── pull_request_template.md
│ └── workflows/
│ └── ci.yml
├── .gitignore
├── .jcheck/
│ └── conf
├── CONTRIBUTING.md
├── LICENSE
├── Makefile
├── README.md
├── Unzip.java
├── args/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── args/
│ │ ├── Argument.java
│ │ ├── ArgumentParser.java
│ │ ├── Arguments.java
│ │ ├── Command.java
│ │ ├── CommandCtor.java
│ │ ├── CommandHelpText.java
│ │ ├── CommandMain.java
│ │ ├── Default.java
│ │ ├── Executable.java
│ │ ├── Flag.java
│ │ ├── FlagValue.java
│ │ ├── Input.java
│ │ ├── InputDescriber.java
│ │ ├── InputQualifier.java
│ │ ├── InputQuantifier.java
│ │ ├── Main.java
│ │ ├── MultiCommandParser.java
│ │ ├── Option.java
│ │ ├── OptionDescribe.java
│ │ ├── OptionFullname.java
│ │ ├── OptionHelptext.java
│ │ ├── OptionQualifier.java
│ │ ├── Switch.java
│ │ ├── SwitchFullname.java
│ │ ├── SwitchHelptext.java
│ │ └── SwitchQualifier.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── args/
│ ├── InputTests.java
│ └── SwitchTests.java
├── bot/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bot/
│ │ ├── Bot.java
│ │ ├── BotConfiguration.java
│ │ ├── BotFactory.java
│ │ ├── BotRunner.java
│ │ ├── BotRunnerConfiguration.java
│ │ ├── BotTaskAggregationHandler.java
│ │ ├── BotWatchdog.java
│ │ ├── ConfigurationError.java
│ │ ├── LivenessHandler.java
│ │ ├── LogContext.java
│ │ ├── LogContextMap.java
│ │ ├── MetricsHandler.java
│ │ ├── ProfileHandler.java
│ │ ├── ReadinessHandler.java
│ │ ├── VersionHandler.java
│ │ ├── WebhookHandler.java
│ │ └── WorkItem.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── bot/
│ ├── BotRunnerConfigurationTests.java
│ ├── BotRunnerTests.java
│ ├── BotTaskAggregationHandlerTests.java
│ └── LogContextTests.java
├── bots/
│ ├── bridgekeeper/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── bridgekeeper/
│ │ │ ├── BridgekeeperBotFactory.java
│ │ │ ├── PullRequestCloserBot.java
│ │ │ └── PullRequestPrunerBot.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── bridgekeeper/
│ │ ├── BridgekeeperBotFactoryTest.java
│ │ ├── PullRequestCloserBotTests.java
│ │ └── PullRequestPrunerBotTests.java
│ ├── censussync/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── censussync/
│ │ │ ├── CensusSyncBotFactory.java
│ │ │ ├── CensusSyncSplitBot.java
│ │ │ └── CensusSyncUnifyBot.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── censussync/
│ │ └── CensusSyncBotFactoryTest.java
│ ├── checkout/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── checkout/
│ │ │ ├── CheckoutBot.java
│ │ │ ├── CheckoutBotFactory.java
│ │ │ └── MarkStorage.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── checkout/
│ │ ├── CheckoutBotFactoryTest.java
│ │ └── CheckoutBotTests.java
│ ├── cli/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── cli/
│ │ │ ├── BotConsoleHandler.java
│ │ │ ├── BotLauncher.java
│ │ │ ├── BotLogstashHandler.java
│ │ │ └── BotSlackHandler.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── cli/
│ │ ├── BotLogstashHandlerTests.java
│ │ ├── BotSlackHandlerTests.java
│ │ ├── LoggingBot.java
│ │ └── RestReceiver.java
│ ├── common/
│ │ ├── build.gradle
│ │ └── src/
│ │ └── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── common/
│ │ ├── BotUtils.java
│ │ ├── CommandNameEnum.java
│ │ ├── PatternEnum.java
│ │ ├── PullRequestConstants.java
│ │ └── SolvesTracker.java
│ ├── forward/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── forward/
│ │ │ ├── ForwardBot.java
│ │ │ └── ForwardBotFactory.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── forward/
│ │ ├── ForwardBotFactoryTest.java
│ │ └── ForwardBotTests.java
│ ├── hgbridge/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── hgbridge/
│ │ │ ├── Exporter.java
│ │ │ ├── ExporterConfig.java
│ │ │ ├── JBridgeBot.java
│ │ │ └── JBridgeBotFactory.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── hgbridge/
│ │ ├── BridgeBotTests.java
│ │ └── JBridgeBotFactoryTest.java
│ ├── merge/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── merge/
│ │ │ ├── Clock.java
│ │ │ ├── MergeBot.java
│ │ │ └── MergeBotFactory.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── merge/
│ │ ├── MergeBotFactoryTest.java
│ │ └── MergeBotTests.java
│ ├── mirror/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── mirror/
│ │ │ ├── MirrorBot.java
│ │ │ └── MirrorBotFactory.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── mirror/
│ │ ├── MirrorBotFactoryTest.java
│ │ └── MirrorBotTests.java
│ ├── mlbridge/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── mlbridge/
│ │ │ ├── ArchiveItem.java
│ │ │ ├── ArchiveMessages.java
│ │ │ ├── ArchiveReaderWorkItem.java
│ │ │ ├── ArchiveWorkItem.java
│ │ │ ├── BridgedComment.java
│ │ │ ├── CensusInstance.java
│ │ │ ├── CommentPosterWorkItem.java
│ │ │ ├── CooldownQuarantine.java
│ │ │ ├── EmojiTable.java
│ │ │ ├── HostUserToEmailAuthor.java
│ │ │ ├── HostUserToRole.java
│ │ │ ├── HostUserToUsername.java
│ │ │ ├── LabelsUpdaterWorkItem.java
│ │ │ ├── MailingListArchiveReaderBot.java
│ │ │ ├── MailingListBridgeBot.java
│ │ │ ├── MailingListBridgeBotBuilder.java
│ │ │ ├── MailingListBridgeBotFactory.java
│ │ │ ├── MailingListConfiguration.java
│ │ │ ├── MarkdownToText.java
│ │ │ ├── QuoteFilter.java
│ │ │ ├── ReviewArchive.java
│ │ │ ├── TextToMarkdown.java
│ │ │ ├── WebrevDescription.java
│ │ │ ├── WebrevNotification.java
│ │ │ └── WebrevStorage.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── mlbridge/
│ │ ├── ArchiveItemTests.java
│ │ ├── BridgedCommentTests.java
│ │ ├── LabelsUpdaterTests.java
│ │ ├── MailingListArchiveReaderBotTests.java
│ │ ├── MailingListBridgeBotFactoryTest.java
│ │ ├── MailingListBridgeBotTests.java
│ │ ├── MarkdownToTextTests.java
│ │ ├── QuoteFilterTests.java
│ │ ├── TextToMarkdownTests.java
│ │ └── WebrevStorageTests.java
│ ├── notify/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── notify/
│ │ │ ├── CommitFormatters.java
│ │ │ ├── Emitter.java
│ │ │ ├── NonRetriableException.java
│ │ │ ├── Notifier.java
│ │ │ ├── NotifierFactory.java
│ │ │ ├── NotifyBot.java
│ │ │ ├── NotifyBotBuilder.java
│ │ │ ├── NotifyBotFactory.java
│ │ │ ├── PullRequestListener.java
│ │ │ ├── PullRequestState.java
│ │ │ ├── PullRequestWorkItem.java
│ │ │ ├── RepositoryListener.java
│ │ │ ├── RepositoryWorkItem.java
│ │ │ ├── UpdateHistory.java
│ │ │ ├── UpdatedBranch.java
│ │ │ ├── UpdatedTag.java
│ │ │ ├── comment/
│ │ │ │ ├── CommitCommentNotifier.java
│ │ │ │ └── CommitCommentNotifierFactory.java
│ │ │ ├── issue/
│ │ │ │ ├── CensusInstance.java
│ │ │ │ ├── IssueNotifier.java
│ │ │ │ ├── IssueNotifierBuilder.java
│ │ │ │ └── IssueNotifierFactory.java
│ │ │ ├── json/
│ │ │ │ ├── JsonNotifier.java
│ │ │ │ ├── JsonNotifierFactory.java
│ │ │ │ └── JsonWriter.java
│ │ │ ├── mailinglist/
│ │ │ │ ├── MailingListNotifier.java
│ │ │ │ ├── MailingListNotifierBuilder.java
│ │ │ │ └── MailingListNotifierFactory.java
│ │ │ ├── notes/
│ │ │ │ ├── CommitNoteNotifier.java
│ │ │ │ └── CommitNoteNotifierFactory.java
│ │ │ ├── prbranch/
│ │ │ │ ├── PullRequestBranchNotifier.java
│ │ │ │ └── PullRequestBranchNotifierFactory.java
│ │ │ └── slack/
│ │ │ ├── SlackNotifier.java
│ │ │ └── SlackNotifierFactory.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── notify/
│ │ ├── NotifyBotFactoryTest.java
│ │ ├── RepositoryWorkItemTests.java
│ │ ├── TestUtils.java
│ │ ├── UpdateHistoryTests.java
│ │ ├── UpdaterTests.java
│ │ ├── comment/
│ │ │ └── CommitCommentNotifierTests.java
│ │ ├── issue/
│ │ │ └── IssueNotifierTests.java
│ │ ├── json/
│ │ │ └── JsonNotifierTests.java
│ │ ├── mailinglist/
│ │ │ └── MailingListNotifierTests.java
│ │ ├── notes/
│ │ │ └── CommitNoteNotiferTests.java
│ │ └── prbranch/
│ │ └── PullRequestBranchNotifierTests.java
│ ├── pr/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── pr/
│ │ │ ├── AdditionalConfiguration.java
│ │ │ ├── Approval.java
│ │ │ ├── ApprovalCommand.java
│ │ │ ├── ApproveCommand.java
│ │ │ ├── AuthorCommand.java
│ │ │ ├── BackportCommand.java
│ │ │ ├── BranchCommand.java
│ │ │ ├── CSRCommand.java
│ │ │ ├── CSRIssueBot.java
│ │ │ ├── CSRIssueWorkItem.java
│ │ │ ├── CensusInstance.java
│ │ │ ├── CheckRun.java
│ │ │ ├── CheckWorkItem.java
│ │ │ ├── CheckablePullRequest.java
│ │ │ ├── CleanCommand.java
│ │ │ ├── CommandExtractor.java
│ │ │ ├── CommandHandler.java
│ │ │ ├── CommandInvocation.java
│ │ │ ├── CommitCommandWorkItem.java
│ │ │ ├── CommitCommentsWorkItem.java
│ │ │ ├── ContributorCommand.java
│ │ │ ├── Contributors.java
│ │ │ ├── IntegrateCommand.java
│ │ │ ├── IntegrationLock.java
│ │ │ ├── IssueBot.java
│ │ │ ├── IssueCommand.java
│ │ │ ├── JEPCommand.java
│ │ │ ├── LabelCommand.java
│ │ │ ├── LabelTracker.java
│ │ │ ├── LabelerWorkItem.java
│ │ │ ├── LimitedCensusInstance.java
│ │ │ ├── MergePullRequestReviewConfiguration.java
│ │ │ ├── OpenCommand.java
│ │ │ ├── OverridingAuthor.java
│ │ │ ├── PRRecord.java
│ │ │ ├── PullRequestBot.java
│ │ │ ├── PullRequestBotBuilder.java
│ │ │ ├── PullRequestBotFactory.java
│ │ │ ├── PullRequestCheckIssueVisitor.java
│ │ │ ├── PullRequestCommandWorkItem.java
│ │ │ ├── PullRequestWorkItem.java
│ │ │ ├── ReadyForSponsorTracker.java
│ │ │ ├── ReviewCoverage.java
│ │ │ ├── ReviewerCommand.java
│ │ │ ├── Reviewers.java
│ │ │ ├── ReviewersCommand.java
│ │ │ ├── ReviewersTracker.java
│ │ │ ├── ScratchArea.java
│ │ │ ├── SponsorCommand.java
│ │ │ ├── Summary.java
│ │ │ ├── SummaryCommand.java
│ │ │ ├── TagCommand.java
│ │ │ ├── TemplateCommand.java
│ │ │ ├── TouchCommand.java
│ │ │ ├── TrailerCommand.java
│ │ │ └── Trailers.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── pr/
│ │ ├── AdditionalConfigurationTests.java
│ │ ├── ApprovalAndApproveCommandTests.java
│ │ ├── ApprovalTests.java
│ │ ├── AuthorCommandTests.java
│ │ ├── BackportCommitCommandTests.java
│ │ ├── BackportPRCommandTests.java
│ │ ├── BackportTests.java
│ │ ├── BranchCommitCommandTests.java
│ │ ├── CSRBotTests.java
│ │ ├── CSRCommandTests.java
│ │ ├── CheckTests.java
│ │ ├── CleanCommandTests.java
│ │ ├── CommitCommandAsserts.java
│ │ ├── CommitCommandTests.java
│ │ ├── ContributorTests.java
│ │ ├── IntegrateTests.java
│ │ ├── IntegrationLockTests.java
│ │ ├── IssueBotTests.java
│ │ ├── IssueTests.java
│ │ ├── JEPCommandTests.java
│ │ ├── LabelTests.java
│ │ ├── LabelerTests.java
│ │ ├── MergeTests.java
│ │ ├── OpenCommandTests.java
│ │ ├── PreIntegrateTests.java
│ │ ├── PullRequestAsserts.java
│ │ ├── PullRequestBotFactoryTest.java
│ │ ├── PullRequestCommandTests.java
│ │ ├── RequiredCheckedLinesTests.java
│ │ ├── ReviewerTests.java
│ │ ├── ReviewersTests.java
│ │ ├── SponsorTests.java
│ │ ├── SummaryTests.java
│ │ ├── TagCommitCommandTests.java
│ │ ├── TemplateCommandTests.java
│ │ └── TrailersTests.java
│ ├── submit/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── submit/
│ │ │ ├── CheckUpdater.java
│ │ │ ├── ShellExecutor.java
│ │ │ ├── ShellExecutorFactory.java
│ │ │ ├── SubmitBot.java
│ │ │ ├── SubmitBotFactory.java
│ │ │ ├── SubmitBotWorkItem.java
│ │ │ ├── SubmitExecutor.java
│ │ │ └── SubmitExecutorFactory.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── submit/
│ │ ├── CheckUpdaterTests.java
│ │ ├── ShellExecutorTests.java
│ │ ├── SubmitBotFactoryTest.java
│ │ └── SubmitBotTests.java
│ ├── synclabel/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── synclabel/
│ │ │ ├── SyncLabelBot.java
│ │ │ ├── SyncLabelBotFactory.java
│ │ │ ├── SyncLabelBotFindMainIssueWorkItem.java
│ │ │ └── SyncLabelBotUpdateLabelWorkItem.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── synclabel/
│ │ ├── SyncLabelBotFactoryTest.java
│ │ └── SyncLabelBotTests.java
│ ├── tester/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── tester/
│ │ │ ├── Stage.java
│ │ │ ├── State.java
│ │ │ ├── TestBot.java
│ │ │ ├── TestBotFactory.java
│ │ │ ├── TestUpdateNeededWorkItem.java
│ │ │ └── TestWorkItem.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── tester/
│ │ ├── InMemoryContinuousIntegration.java
│ │ ├── InMemoryHost.java
│ │ ├── InMemoryHostedRepository.java
│ │ ├── InMemoryJob.java
│ │ ├── InMemoryPullRequest.java
│ │ ├── StateTests.java
│ │ ├── TestBotFactoryTest.java
│ │ ├── TestBotTests.java
│ │ └── TestWorkItemTests.java
│ ├── testinfo/
│ │ ├── build.gradle
│ │ └── src/
│ │ ├── main/
│ │ │ └── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── bots/
│ │ │ └── testinfo/
│ │ │ ├── TestInfoBot.java
│ │ │ ├── TestInfoBotFactory.java
│ │ │ ├── TestInfoBotWorkItem.java
│ │ │ └── TestResults.java
│ │ └── test/
│ │ └── java/
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── testinfo/
│ │ ├── TestInfoBotFactoryTest.java
│ │ ├── TestInfoTests.java
│ │ └── TestResultsTests.java
│ └── topological/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── bots/
│ │ └── topological/
│ │ ├── Edge.java
│ │ ├── TopologicalBot.java
│ │ ├── TopologicalBotFactory.java
│ │ └── TopologicalSort.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── bots/
│ └── topological/
│ ├── TopologicalBotFactoryTest.java
│ ├── TopologicalBotTests.java
│ └── TopologicalSortTest.java
├── bots.dockerfile
├── build.gradle
├── census/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── census/
│ │ ├── Census.java
│ │ ├── Contributor.java
│ │ ├── Contributors.java
│ │ ├── Group.java
│ │ ├── Member.java
│ │ ├── Namespace.java
│ │ ├── Parser.java
│ │ ├── Project.java
│ │ └── Version.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── census/
│ ├── CensusTests.java
│ ├── GroupTests.java
│ ├── MemberTests.java
│ └── ProjectTests.java
├── ci/
│ ├── build.gradle
│ └── src/
│ └── main/
│ └── java/
│ ├── module-info.java
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── ci/
│ ├── Build.java
│ ├── ContinuousIntegration.java
│ ├── ContinuousIntegrationFactory.java
│ ├── Job.java
│ └── Test.java
├── cli/
│ ├── build.gradle
│ ├── resources/
│ │ └── man/
│ │ └── man1/
│ │ ├── git-jcheck.1
│ │ ├── git-verify-import.1
│ │ └── git-webrev.1
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── cli/
│ │ ├── ForgeUtils.java
│ │ ├── GitBackport.java
│ │ ├── GitCommitComments.java
│ │ ├── GitCredentials.java
│ │ ├── GitDefpath.java
│ │ ├── GitFork.java
│ │ ├── GitHgExport.java
│ │ ├── GitInfo.java
│ │ ├── GitJCheck.java
│ │ ├── GitPr.java
│ │ ├── GitProxy.java
│ │ ├── GitPublish.java
│ │ ├── GitSkara.java
│ │ ├── GitSync.java
│ │ ├── GitToken.java
│ │ ├── GitTranslate.java
│ │ ├── GitTrees.java
│ │ ├── GitWebrev.java
│ │ ├── JCheckCLIVisitor.java
│ │ ├── Logging.java
│ │ ├── MinimalFormatter.java
│ │ ├── Remote.java
│ │ ├── SkaraDebug.java
│ │ ├── debug/
│ │ │ ├── GitMlRules.java
│ │ │ ├── GitOpenJDKImport.java
│ │ │ ├── GitVerifyImport.java
│ │ │ ├── HgOpenJDKImport.java
│ │ │ ├── IssueRedecorate.java
│ │ │ └── SkaraDebugHelp.java
│ │ └── pr/
│ │ ├── GitPrApply.java
│ │ ├── GitPrCC.java
│ │ ├── GitPrCSR.java
│ │ ├── GitPrCheckout.java
│ │ ├── GitPrClose.java
│ │ ├── GitPrContributor.java
│ │ ├── GitPrCreate.java
│ │ ├── GitPrFetch.java
│ │ ├── GitPrHelp.java
│ │ ├── GitPrInfo.java
│ │ ├── GitPrIntegrate.java
│ │ ├── GitPrIssue.java
│ │ ├── GitPrList.java
│ │ ├── GitPrReview.java
│ │ ├── GitPrReviewer.java
│ │ ├── GitPrSet.java
│ │ ├── GitPrShow.java
│ │ ├── GitPrSponsor.java
│ │ ├── GitPrSummary.java
│ │ ├── GitPrTest.java
│ │ └── Utils.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── cli/
│ └── debug/
│ └── TestGitMlRules.java
├── config/
│ └── mailinglist/
│ └── rules/
│ └── jdk.json
├── deps.env
├── email/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── email/
│ │ ├── Email.java
│ │ ├── EmailAddress.java
│ │ ├── EmailBuilder.java
│ │ ├── MimeText.java
│ │ ├── SMTP.java
│ │ └── SMTPSession.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── email/
│ ├── EmailAddressTests.java
│ ├── EmailTests.java
│ ├── MimeTextTests.java
│ └── SMTPTests.java
├── encoding/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── encoding/
│ │ └── Base85.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── encoding/
│ └── Base85Tests.java
├── forge/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── forge/
│ │ ├── Check.java
│ │ ├── CheckAnnotation.java
│ │ ├── CheckAnnotationBuilder.java
│ │ ├── CheckAnnotationLevel.java
│ │ ├── CheckBuilder.java
│ │ ├── CheckStatus.java
│ │ ├── Collaborator.java
│ │ ├── CommitComment.java
│ │ ├── CommitFailure.java
│ │ ├── Forge.java
│ │ ├── ForgeFactory.java
│ │ ├── HostedBranch.java
│ │ ├── HostedCommit.java
│ │ ├── HostedRepository.java
│ │ ├── HostedRepositoryPool.java
│ │ ├── LabelConfiguration.java
│ │ ├── LabelConfigurationHostedRepository.java
│ │ ├── LabelConfigurationJson.java
│ │ ├── MemberState.java
│ │ ├── PreIntegrations.java
│ │ ├── PullRequest.java
│ │ ├── PullRequestBody.java
│ │ ├── PullRequestPoller.java
│ │ ├── PullRequestUpdateCache.java
│ │ ├── PullRequestUtils.java
│ │ ├── ReferenceChange.java
│ │ ├── Review.java
│ │ ├── ReviewComment.java
│ │ ├── WebHook.java
│ │ ├── WorkflowStatus.java
│ │ ├── bitbucket/
│ │ │ ├── BitbucketForgeFactory.java
│ │ │ ├── BitbucketHost.java
│ │ │ └── BitbucketRepository.java
│ │ ├── github/
│ │ │ ├── GitHubApplication.java
│ │ │ ├── GitHubForgeFactory.java
│ │ │ ├── GitHubHost.java
│ │ │ ├── GitHubPullRequest.java
│ │ │ └── GitHubRepository.java
│ │ ├── gitlab/
│ │ │ ├── GitLabForgeFactory.java
│ │ │ ├── GitLabHost.java
│ │ │ ├── GitLabMergeRequest.java
│ │ │ └── GitLabRepository.java
│ │ └── internal/
│ │ └── ForgeUtils.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── forge/
│ ├── CheckBuilderTests.java
│ ├── ForgeIntegrationTests.java
│ ├── ForgeTests.java
│ ├── HostedRepositoryPoolTests.java
│ ├── LabelConfigurationTests.java
│ ├── PullRequestBodyTests.java
│ ├── PullRequestPollerTests.java
│ ├── PullRequestTests.java
│ ├── PullRequestUtilsTests.java
│ ├── bitbucket/
│ │ └── BitbucketForgeFactoryTests.java
│ ├── github/
│ │ ├── GitHubApplicationTests.java
│ │ ├── GitHubForgeFactoryTests.java
│ │ ├── GitHubHostTests.java
│ │ └── GitHubIntegrationTests.java
│ └── gitlab/
│ ├── GitLabForgeFactoryTests.java
│ └── GitLabIntegrationTests.java
├── gradle/
│ └── wrapper/
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── host/
│ ├── build.gradle
│ └── src/
│ └── main/
│ └── java/
│ ├── module-info.java
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── host/
│ ├── Credential.java
│ ├── Host.java
│ └── HostUser.java
├── ini/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── ini/
│ │ ├── INI.java
│ │ └── Section.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── ini/
│ └── INITests.java
├── issuetracker/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── issuetracker/
│ │ ├── ActiveUserTracker.java
│ │ ├── Comment.java
│ │ ├── Issue.java
│ │ ├── IssueLinkBuilder.java
│ │ ├── IssueProject.java
│ │ ├── IssueProjectPoller.java
│ │ ├── IssueTracker.java
│ │ ├── IssueTrackerFactory.java
│ │ ├── IssueTrackerIssue.java
│ │ ├── Label.java
│ │ ├── Link.java
│ │ ├── WebLinkBuilder.java
│ │ └── jira/
│ │ ├── JiraHost.java
│ │ ├── JiraIssue.java
│ │ ├── JiraIssueTrackerFactory.java
│ │ ├── JiraLinkType.java
│ │ ├── JiraProject.java
│ │ └── JiraVault.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── issuetracker/
│ ├── IssueProjectPollerTests.java
│ ├── IssueTrackerTests.java
│ └── jira/
│ └── JiraIntegrationTests.java
├── jbs/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── jbs/
│ │ ├── Backports.java
│ │ ├── BuildCompare.java
│ │ └── JdkVersion.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── jbs/
│ ├── BackportsIntegrationTests.java
│ ├── BackportsTests.java
│ ├── BuildCompareTests.java
│ └── JdkVersionTests.java
├── jcheck/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── jcheck/
│ │ ├── AuthorCheck.java
│ │ ├── AuthorEmailIssue.java
│ │ ├── AuthorNameIssue.java
│ │ ├── BinaryCheck.java
│ │ ├── BinaryIssue.java
│ │ ├── BranchIssue.java
│ │ ├── BranchesCheck.java
│ │ ├── CensusConfiguration.java
│ │ ├── Check.java
│ │ ├── ChecksConfiguration.java
│ │ ├── CommitCheck.java
│ │ ├── CommitIssue.java
│ │ ├── CommitterCheck.java
│ │ ├── CommitterConfiguration.java
│ │ ├── CommitterEmailIssue.java
│ │ ├── CommitterIssue.java
│ │ ├── CommitterNameIssue.java
│ │ ├── CopyrightFormatCheck.java
│ │ ├── CopyrightFormatConfiguration.java
│ │ ├── CopyrightFormatIssue.java
│ │ ├── DuplicateIssuesCheck.java
│ │ ├── DuplicateIssuesIssue.java
│ │ ├── ExecutableCheck.java
│ │ ├── ExecutableIssue.java
│ │ ├── GeneralConfiguration.java
│ │ ├── HgTagCommitCheck.java
│ │ ├── HgTagCommitIssue.java
│ │ ├── InvalidReviewersIssue.java
│ │ ├── Issue.java
│ │ ├── IssueVisitor.java
│ │ ├── IssuesCheck.java
│ │ ├── IssuesConfiguration.java
│ │ ├── IssuesIssue.java
│ │ ├── IssuesTitleCheck.java
│ │ ├── IssuesTitleIssue.java
│ │ ├── JCheck.java
│ │ ├── JCheckConfiguration.java
│ │ ├── MergeConfiguration.java
│ │ ├── MergeMessageCheck.java
│ │ ├── MergeMessageIssue.java
│ │ ├── MessageCheck.java
│ │ ├── MessageIssue.java
│ │ ├── MessageWhitespaceIssue.java
│ │ ├── ProblemListsCheck.java
│ │ ├── ProblemListsConfiguration.java
│ │ ├── ProblemListsIssue.java
│ │ ├── RepositoryCheck.java
│ │ ├── RepositoryConfiguration.java
│ │ ├── ReviewersCheck.java
│ │ ├── ReviewersConfiguration.java
│ │ ├── SelfReviewIssue.java
│ │ ├── Severity.java
│ │ ├── SymlinkCheck.java
│ │ ├── SymlinkIssue.java
│ │ ├── TagIssue.java
│ │ ├── TagsCheck.java
│ │ ├── TooFewReviewersIssue.java
│ │ ├── Utilities.java
│ │ ├── WhitespaceCheck.java
│ │ ├── WhitespaceConfiguration.java
│ │ ├── WhitespaceIssue.java
│ │ └── iterators/
│ │ ├── ConcatIterator.java
│ │ ├── FlatMapIterator.java
│ │ └── MapIterator.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── jcheck/
│ ├── AuthorCheckTests.java
│ ├── BinaryCheckTests.java
│ ├── BranchesCheckTests.java
│ ├── CommitterCheckTests.java
│ ├── CopyrightFormatCheckTests.java
│ ├── DuplicateIssuesCheckTests.java
│ ├── ExecutableCheckTests.java
│ ├── HgTagCommitCheckTests.java
│ ├── IssuesCheckTests.java
│ ├── JCheckTests.java
│ ├── MergeMessageCheckTests.java
│ ├── MessageCheckTests.java
│ ├── ProblemListsCheckTests.java
│ ├── ReviewersCheckTests.java
│ ├── SymlinkCheckTests.java
│ ├── TagsCheckTests.java
│ ├── TestRepository.java
│ └── WhitespaceCheckTests.java
├── json/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── json/
│ │ ├── JSON.java
│ │ ├── JSONArray.java
│ │ ├── JSONBoolean.java
│ │ ├── JSONDecimal.java
│ │ ├── JSONNull.java
│ │ ├── JSONNumber.java
│ │ ├── JSONObject.java
│ │ ├── JSONParser.java
│ │ ├── JSONString.java
│ │ ├── JSONValue.java
│ │ └── JWCC.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── json/
│ ├── JSONParserTests.java
│ └── JWCCTests.java
├── mailinglist/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── mailinglist/
│ │ ├── Conversation.java
│ │ ├── MailingListReader.java
│ │ ├── MailingListServer.java
│ │ ├── MailingListServerFactory.java
│ │ ├── Mbox.java
│ │ ├── mailman/
│ │ │ ├── Mailman2Server.java
│ │ │ ├── Mailman3Server.java
│ │ │ ├── MailmanListReader.java
│ │ │ ├── MailmanServer.java
│ │ │ └── SendOnlyServer.java
│ │ └── mboxfile/
│ │ ├── MboxFileListReader.java
│ │ └── MboxFileListServer.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── mailinglist/
│ ├── Mailman2Tests.java
│ ├── Mailman3IntegrationTests.java
│ ├── Mailman3Tests.java
│ └── MboxTests.java
├── metrics/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── metrics/
│ │ ├── Collector.java
│ │ ├── CollectorRegistry.java
│ │ ├── Counter.java
│ │ ├── Exporter.java
│ │ ├── Gauge.java
│ │ ├── Metric.java
│ │ └── PrometheusExporter.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── metrics/
│ ├── CollectorRegistryTests.java
│ ├── CounterTests.java
│ ├── GaugeTests.java
│ └── PrometheusExpoterTests.java
├── network/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── network/
│ │ ├── RestRequest.java
│ │ ├── RestRequestCache.java
│ │ ├── URIBuilder.java
│ │ └── UncheckedRestException.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── network/
│ ├── RestRequestTests.java
│ └── URIBuilderTests.java
├── process/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── process/
│ │ ├── Execution.java
│ │ └── Process.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── process/
│ └── ProcessTests.java
├── proxy/
│ ├── build.gradle
│ └── src/
│ └── main/
│ └── java/
│ ├── module-info.java
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── proxy/
│ └── HttpProxy.java
├── settings.gradle
├── skara.gitconfig
├── skara.py
├── skara.sh
├── storage/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ └── java/
│ │ ├── module-info.java
│ │ └── org/
│ │ └── openjdk/
│ │ └── skara/
│ │ └── storage/
│ │ ├── FileStorage.java
│ │ ├── HostedRepositoryStorage.java
│ │ ├── RepositoryStorage.java
│ │ ├── Storage.java
│ │ ├── StorageBuilder.java
│ │ ├── StorageDeserializer.java
│ │ └── StorageSerializer.java
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── storage/
│ ├── FileStorageTests.java
│ ├── HostedRepositoryStorageTests.java
│ └── RepositoryStorageTests.java
├── test/
│ ├── build.gradle
│ └── src/
│ └── main/
│ └── java/
│ ├── module-info.java
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── test/
│ ├── CensusBuilder.java
│ ├── CheckableRepository.java
│ ├── DisableAllBotsTestsOnWindows.java
│ ├── EnabledIfTestProperties.java
│ ├── HostCredentials.java
│ ├── SMTPServer.java
│ ├── TemporaryDirectory.java
│ ├── TestBotFactory.java
│ ├── TestBotRunner.java
│ ├── TestHost.java
│ ├── TestHostedRepository.java
│ ├── TestIssue.java
│ ├── TestIssueProject.java
│ ├── TestIssueStore.java
│ ├── TestIssueTrackerIssue.java
│ ├── TestIssueTrackerIssueStore.java
│ ├── TestMailmanServer.java
│ ├── TestProperties.java
│ ├── TestPullRequest.java
│ ├── TestPullRequestStore.java
│ ├── TestWebrevServer.java
│ └── TestableRepository.java
├── test.dockerfile
├── vcs/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── vcs/
│ │ │ ├── Author.java
│ │ │ ├── BinaryHunk.java
│ │ │ ├── BinaryPatch.java
│ │ │ ├── Bookmark.java
│ │ │ ├── Branch.java
│ │ │ ├── Commit.java
│ │ │ ├── CommitMetadata.java
│ │ │ ├── Commits.java
│ │ │ ├── Diff.java
│ │ │ ├── DiffComparator.java
│ │ │ ├── FileEntry.java
│ │ │ ├── FileType.java
│ │ │ ├── Hash.java
│ │ │ ├── Hunk.java
│ │ │ ├── Patch.java
│ │ │ ├── Range.java
│ │ │ ├── ReadOnlyRepository.java
│ │ │ ├── Reference.java
│ │ │ ├── Repository.java
│ │ │ ├── Status.java
│ │ │ ├── StatusEntry.java
│ │ │ ├── Submodule.java
│ │ │ ├── Tag.java
│ │ │ ├── TextualPatch.java
│ │ │ ├── Tree.java
│ │ │ ├── UnifiedDiffParser.java
│ │ │ ├── VCS.java
│ │ │ ├── WebrevStats.java
│ │ │ ├── git/
│ │ │ │ ├── GitCombinedDiffParser.java
│ │ │ │ ├── GitCommitIterator.java
│ │ │ │ ├── GitCommitMetadata.java
│ │ │ │ ├── GitCommits.java
│ │ │ │ ├── GitRepository.java
│ │ │ │ └── GitVersion.java
│ │ │ ├── hg/
│ │ │ │ ├── HgCommitIterator.java
│ │ │ │ ├── HgCommitMetadata.java
│ │ │ │ ├── HgCommits.java
│ │ │ │ └── HgRepository.java
│ │ │ ├── openjdk/
│ │ │ │ ├── CommitMessage.java
│ │ │ │ ├── CommitMessageBuilder.java
│ │ │ │ ├── CommitMessageFormatter.java
│ │ │ │ ├── CommitMessageFormatters.java
│ │ │ │ ├── CommitMessageParser.java
│ │ │ │ ├── CommitMessageParsers.java
│ │ │ │ ├── CommitMessageSyntax.java
│ │ │ │ ├── Issue.java
│ │ │ │ ├── OpenJDKTag.java
│ │ │ │ └── convert/
│ │ │ │ ├── Attribution.java
│ │ │ │ ├── Converter.java
│ │ │ │ ├── ConverterCommitMessageParser.java
│ │ │ │ ├── GitCommitMetadata.java
│ │ │ │ ├── GitToHgConverter.java
│ │ │ │ ├── HgToGitConverter.java
│ │ │ │ ├── Mark.java
│ │ │ │ └── Pipe.java
│ │ │ └── tools/
│ │ │ ├── GitRawDiffParser.java
│ │ │ ├── PatchHeader.java
│ │ │ └── UnixStreamReader.java
│ │ └── resources/
│ │ └── ext.py
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── vcs/
│ ├── AuthorTests.java
│ ├── RepositoryTests.java
│ ├── UnifiedDiffParserTests.java
│ ├── git/
│ │ └── GitVersionTest.java
│ └── openjdk/
│ ├── CommitMessageBuilderTests.java
│ ├── CommitMessageFormattersTests.java
│ ├── CommitMessageParsersTests.java
│ ├── IssueTests.java
│ ├── OpenJDKTagTests.java
│ └── converter/
│ ├── GitToHgConverterTests.java
│ └── HgToGitConverterTests.java
├── version/
│ ├── build.gradle
│ └── src/
│ └── main/
│ └── java/
│ ├── module-info.java
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── version/
│ └── Version.java
├── webrev/
│ ├── build.gradle
│ └── src/
│ ├── main/
│ │ ├── java/
│ │ │ ├── module-info.java
│ │ │ └── org/
│ │ │ └── openjdk/
│ │ │ └── skara/
│ │ │ └── webrev/
│ │ │ ├── AddedFileView.java
│ │ │ ├── AddedPatchView.java
│ │ │ ├── CDiffView.java
│ │ │ ├── DiffTooLargeException.java
│ │ │ ├── FileView.java
│ │ │ ├── FramesView.java
│ │ │ ├── FullView.java
│ │ │ ├── HTML.java
│ │ │ ├── HunkCoalescer.java
│ │ │ ├── IndexView.java
│ │ │ ├── MetadataFormatter.java
│ │ │ ├── ModifiedFileView.java
│ │ │ ├── Navigation.java
│ │ │ ├── PatchView.java
│ │ │ ├── RawView.java
│ │ │ ├── RemovedFileView.java
│ │ │ ├── RemovedPatchView.java
│ │ │ ├── SDiffView.java
│ │ │ ├── Stats.java
│ │ │ ├── Template.java
│ │ │ ├── UDiffView.java
│ │ │ ├── View.java
│ │ │ ├── ViewUtils.java
│ │ │ ├── Webrev.java
│ │ │ └── WebrevMetaData.java
│ │ └── resources/
│ │ ├── navigation.html
│ │ ├── navigation.js
│ │ └── style.css
│ └── test/
│ └── java/
│ └── org/
│ └── openjdk/
│ └── skara/
│ └── webrev/
│ ├── HTMLTests.java
│ ├── HunkCoalescerTest.java
│ └── WebrevTests.java
└── xml/
├── build.gradle
└── src/
└── main/
└── java/
├── module-info.java
└── org/
└── openjdk/
└── skara/
└── xml/
└── XML.java
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
#
# Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation.
#
# This code is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
#
**/.*
**/build/
**/out/
**/bin/
================================================
FILE: .gitattributes
================================================
#
# Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation.
#
# This code is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
#
gradlew.bat text eol=crlf
================================================
FILE: .github/pull_request_template.md
================================================
---------
- [ ] I confirm that I make this contribution in accordance with the [OpenJDK Interim AI Policy](https://openjdk.org/legal/ai).
================================================
FILE: .github/workflows/ci.yml
================================================
#
# The pre-submit tests will only runs for forks of the TARGET_PROJECT defined below. This is set to "skara" by default,
# and can be changed by downstream projects if they also want to run pre-submit tests.
#
# The tests will attempt to merge the latest commits from TARGET_BRANCH before executing, to ensure that what is tested
# is as close as possible to what the final integration result will be. This is set to "master" by default, and can
# be changed by downstream projects that utilize multiple branches in order to select the correct one.
#
name: Pre-submit tests
on:
push:
branches-ignore:
- master
- pr/*
jobs:
prerequisites:
name: Prerequisites
runs-on: "ubuntu-latest"
env:
TARGET_PROJECT: skara
TARGET_BRANCH: master
outputs:
should_run: ${{ steps.check_submit.outputs.should_run }}
fetch_target_command: ${{ steps.merge_target.outputs.command }}
merge_target_command: ${{ steps.try_merge_target.outputs.command }}
steps:
- name: Determine target project name (fork source)
id: upstream_repo
uses: actions/github-script@v7
with:
result-encoding: string
script: "return (await github.rest.repos.get( {owner: context.repo.owner, repo: context.repo.repo })).data.source.name"
- name: Check if submit tests should actually run
id: check_submit
run: echo "should_run=${{ env.TARGET_PROJECT == steps.upstream_repo.outputs.result }}" >> $GITHUB_OUTPUT
- name: Checkout the source
uses: actions/checkout@v4
with:
fetch-depth: 1000
if: steps.check_submit.outputs.should_run != 'false'
- name: Determine merge target hash
id: merge_target
run: |
git fetch https://github.com/openjdk/${{ steps.upstream_repo.outputs.result }} ${TARGET_BRANCH}
echo "hash=`git rev-parse FETCH_HEAD`" >> $GITHUB_OUTPUT
echo "command=git fetch https://github.com/openjdk/${{ steps.upstream_repo.outputs.result }} ${TARGET_BRANCH}" >> $GITHUB_OUTPUT
if: steps.check_submit.outputs.should_run != 'false'
- name: Determine merge strategy
id: try_merge_target
run: >
(git -c user.name="presubmit" -c user.email="presubmit@github.actions" merge --no-edit ${{ steps.merge_target.outputs.hash }} &&
(echo "command=git -c user.name="presubmit" -c user.email="presubmit@github.actions" merge --no-edit ${{ steps.merge_target.outputs.hash }}") >> $GITHUB_OUTPUT) ||
(git merge --abort && git -c user.name="presubmit" -c user.email="presubmit@github.actions" rebase ${{ steps.merge_target.outputs.hash }} &&
(echo "command=git -c user.name="presubmit" -c user.email="presubmit@github.actions" rebase ${{ steps.merge_target.outputs.hash }}") >> $GITHUB_OUTPUT) ||
(echo "command=echo There are merge conflicts with the target that will have to be resolved before integration" >> $GITHUB_OUTPUT)
linux:
name: Linux x64
runs-on: "ubuntu-22.04"
needs: prerequisites
if: needs.prerequisites.outputs.should_run
steps:
- name: Checkout the source
uses: actions/checkout@v4
with:
fetch-depth: 1000
- name: Merge latest changes from target branch
run: |
${{ needs.prerequisites.outputs.fetch_target_command }}
${{ needs.prerequisites.outputs.merge_target_command }}
- name: Build and test
run: sh gradlew test local --info --stacktrace
mac:
name: macOS x64
runs-on: "macos-14"
needs: prerequisites
steps:
- name: Checkout the source
uses: actions/checkout@v4
with:
fetch-depth: 1000
- name: Merge latest changes from target branch
run: |
${{ needs.prerequisites.outputs.fetch_target_command }}
${{ needs.prerequisites.outputs.merge_target_command }}
- name: Install Mercurial
run: brew install mercurial
- name: Build and test
run: sh gradlew test local --info --stacktrace
win:
name: Windows x64
runs-on: "windows-2025"
needs: prerequisites
steps:
- name: Checkout the source
uses: actions/checkout@v4
with:
fetch-depth: 1000
- name: Merge latest changes from target branch
run: |
${{ needs.prerequisites.outputs.fetch_target_command }}
${{ needs.prerequisites.outputs.merge_target_command }}
- name: Build and test
run: gradlew.bat test local --info --stacktrace
shell: cmd
================================================
FILE: .gitignore
================================================
#
# Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation.
#
# This code is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
#
.gradle
.jdk
.jib
.classpath
.idea
.project
.settings
*.iml
build/
out/
bin/
test.properties
================================================
FILE: .jcheck/conf
================================================
;
; Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
; DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
;
; This code is free software; you can redistribute it and/or modify it
; under the terms of the GNU General Public License version 2 only, as
; published by the Free Software Foundation.
;
; This code is distributed in the hope that it will be useful, but WITHOUT
; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
; version 2 for more details (a copy is included in the LICENSE file that
; accompanied this code).
;
; You should have received a copy of the GNU General Public License version
; 2 along with this work; if not, write to the Free Software Foundation,
; Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
;
; Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
; or visit www.oracle.com if you need additional information or have any
; questions.
;
[general]
project=skara
repository=skara
jbs=skara
version=1.0
[checks]
error=author,reviewers,whitespace
warning=copyright
[census]
version=0
domain=openjdk.org
[checks "whitespace"]
files=.*\.java$|.*\.yml$|.*\.gradle$|.*.\txt$
[checks "reviewers"]
reviewers=1
[checks "copyright"]
files=.*\.java|.*\.gradle|.*\.sh|.*\.bat|.*\.py|.*\.css|.*\.html|.*\.dockerfile|.*\.gitconfig|Makefile
oracle_locator=.*Copyright \(c\)(.*)Oracle and/or its affiliates\. All rights reserved\.
oracle_validator=.*Copyright \(c\) (\d{4})(?:, (\d{4}))?, Oracle and/or its affiliates\. All rights reserved\.
oracle_required=true
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Thank you for considering contributing to project
[Skara](https://openjdk.org/projects/skara)! For information about
contributing to [OpenJDK](https://openjdk.org/) projects, which include
Skara, please see .
## Mailing List
Project Skara happily accept contributions in the forms of patches sent to
our mailing list, `skara-dev@openjdk.org`. See
for instructions
on how to subscribe of if you want to read the archives
## Pull Requests
Project Skara also gladly accepts contributions in the form of pull requests
on [GitHub](https://github.com/openjdk/skara/pulls/).
## Issues
You can find open issues to work on in the Skara project in the
[JDK Bug System](https://bugs.openjdk.org/):
.
## Larger Contributions
If you have a larger contribution in mind then we highly encourage you to first
discuss your changes on the Skara mailing list, `skara-dev@openjdk.org`,
_before_ you start to write the code.
## Questions
If you have a question or need help, please send an email to our mailing list
`skara-dev@openjdk.org` or stop by the IRC channel `#openjdk` on
[OFTC](https://www.oftc.net/) (see for details).
================================================
FILE: LICENSE
================================================
The GNU General Public License (GPL)
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies of this license
document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your freedom to share
and change it. By contrast, the GNU General Public License is intended to
guarantee your freedom to share and change free software--to make sure the
software is free for all its users. This General Public License applies to
most of the Free Software Foundation's software and to any other program whose
authors commit to using it. (Some other Free Software Foundation software is
covered by the GNU Library General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not price. Our
General Public Licenses are designed to make sure that you have the freedom to
distribute copies of free software (and charge for this service if you wish),
that you receive source code or can get it if you want it, that you can change
the software or use pieces of it in new free programs; and that you know you
can do these things.
To protect your rights, we need to make restrictions that forbid anyone to deny
you these rights or to ask you to surrender the rights. These restrictions
translate to certain responsibilities for you if you distribute copies of the
software, or if you modify it.
For example, if you distribute copies of such a program, whether gratis or for
a fee, you must give the recipients all the rights that you have. You must
make sure that they, too, receive or can get the source code. And you must
show them these terms so they know their rights.
We protect your rights with two steps: (1) copyright the software, and (2)
offer you this license which gives you legal permission to copy, distribute
and/or modify the software.
Also, for each author's protection and ours, we want to make certain that
everyone understands that there is no warranty for this free software. If the
software is modified by someone else and passed on, we want its recipients to
know that what they have is not the original, so that any problems introduced
by others will not reflect on the original authors' reputations.
Finally, any free program is threatened constantly by software patents. We
wish to avoid the danger that redistributors of a free program will
individually obtain patent licenses, in effect making the program proprietary.
To prevent this, we have made it clear that any patent must be licensed for
everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and modification
follow.
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains a notice
placed by the copyright holder saying it may be distributed under the terms of
this General Public License. The "Program", below, refers to any such program
or work, and a "work based on the Program" means either the Program or any
derivative work under copyright law: that is to say, a work containing the
Program or a portion of it, either verbatim or with modifications and/or
translated into another language. (Hereinafter, translation is included
without limitation in the term "modification".) Each licensee is addressed as
"you".
Activities other than copying, distribution and modification are not covered by
this License; they are outside its scope. The act of running the Program is
not restricted, and the output from the Program is covered only if its contents
constitute a work based on the Program (independent of having been made by
running the Program). Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's source code as
you receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice and
disclaimer of warranty; keep intact all the notices that refer to this License
and to the absence of any warranty; and give any other recipients of the
Program a copy of this License along with the Program.
You may charge a fee for the physical act of transferring a copy, and you may
at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion of it, thus
forming a work based on the Program, and copy and distribute such modifications
or work under the terms of Section 1 above, provided that you also meet all of
these conditions:
a) You must cause the modified files to carry prominent notices stating
that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in whole or
in part contains or is derived from the Program or any part thereof, to be
licensed as a whole at no charge to all third parties under the terms of
this License.
c) If the modified program normally reads commands interactively when run,
you must cause it, when started running for such interactive use in the
most ordinary way, to print or display an announcement including an
appropriate copyright notice and a notice that there is no warranty (or
else, saying that you provide a warranty) and that users may redistribute
the program under these conditions, and telling the user how to view a copy
of this License. (Exception: if the Program itself is interactive but does
not normally print such an announcement, your work based on the Program is
not required to print an announcement.)
These requirements apply to the modified work as a whole. If identifiable
sections of that work are not derived from the Program, and can be reasonably
considered independent and separate works in themselves, then this License, and
its terms, do not apply to those sections when you distribute them as separate
works. But when you distribute the same sections as part of a whole which is a
work based on the Program, the distribution of the whole must be on the terms
of this License, whose permissions for other licensees extend to the entire
whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest your
rights to work written entirely by you; rather, the intent is to exercise the
right to control the distribution of derivative or collective works based on
the Program.
In addition, mere aggregation of another work not based on the Program with the
Program (or with a work based on the Program) on a volume of a storage or
distribution medium does not bring the other work under the scope of this
License.
3. You may copy and distribute the Program (or a work based on it, under
Section 2) in object code or executable form under the terms of Sections 1 and
2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable source
code, which must be distributed under the terms of Sections 1 and 2 above
on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three years, to
give any third party, for a charge no more than your cost of physically
performing source distribution, a complete machine-readable copy of the
corresponding source code, to be distributed under the terms of Sections 1
and 2 above on a medium customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer to
distribute corresponding source code. (This alternative is allowed only
for noncommercial distribution and only if you received the program in
object code or executable form with such an offer, in accord with
Subsection b above.)
The source code for a work means the preferred form of the work for making
modifications to it. For an executable work, complete source code means all
the source code for all modules it contains, plus any associated interface
definition files, plus the scripts used to control compilation and installation
of the executable. However, as a special exception, the source code
distributed need not include anything that is normally distributed (in either
source or binary form) with the major components (compiler, kernel, and so on)
of the operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the source
code from the same place counts as distribution of the source code, even though
third parties are not compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program except as
expressly provided under this License. Any attempt otherwise to copy, modify,
sublicense or distribute the Program is void, and will automatically terminate
your rights under this License. However, parties who have received copies, or
rights, from you under this License will not have their licenses terminated so
long as such parties remain in full compliance.
5. You are not required to accept this License, since you have not signed it.
However, nothing else grants you permission to modify or distribute the Program
or its derivative works. These actions are prohibited by law if you do not
accept this License. Therefore, by modifying or distributing the Program (or
any work based on the Program), you indicate your acceptance of this License to
do so, and all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the Program),
the recipient automatically receives a license from the original licensor to
copy, distribute or modify the Program subject to these terms and conditions.
You may not impose any further restrictions on the recipients' exercise of the
rights granted herein. You are not responsible for enforcing compliance by
third parties to this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues), conditions
are imposed on you (whether by court order, agreement or otherwise) that
contradict the conditions of this License, they do not excuse you from the
conditions of this License. If you cannot distribute so as to satisfy
simultaneously your obligations under this License and any other pertinent
obligations, then as a consequence you may not distribute the Program at all.
For example, if a patent license would not permit royalty-free redistribution
of the Program by all those who receive copies directly or indirectly through
you, then the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply and
the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any patents or
other property right claims or to contest validity of any such claims; this
section has the sole purpose of protecting the integrity of the free software
distribution system, which is implemented by public license practices. Many
people have made generous contributions to the wide range of software
distributed through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing to
distribute software through any other system and a licensee cannot impose that
choice.
This section is intended to make thoroughly clear what is believed to be a
consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in certain
countries either by patents or by copyrighted interfaces, the original
copyright holder who places the Program under this License may add an explicit
geographical distribution limitation excluding those countries, so that
distribution is permitted only in or among countries not thus excluded. In
such case, this License incorporates the limitation as if written in the body
of this License.
9. The Free Software Foundation may publish revised and/or new versions of the
General Public License from time to time. Such new versions will be similar in
spirit to the present version, but may differ in detail to address new problems
or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any later
version", you have the option of following the terms and conditions either of
that version or of any later version published by the Free Software Foundation.
If the Program does not specify a version number of this License, you may
choose any version ever published by the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free programs
whose distribution conditions are different, write to the author to ask for
permission. For software which is copyrighted by the Free Software Foundation,
write to the Free Software Foundation; we sometimes make exceptions for this.
Our decision will be guided by the two goals of preserving the free status of
all derivatives of our free software and of promoting the sharing and reuse of
software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE
STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE
PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE,
YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE
PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA
BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER
OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest possible
use to the public, the best way to achieve this is to make it free software
which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest to attach
them to the start of each source file to most effectively convey the exclusion
of warranty; and each file should have at least the "copyright" line and a
pointer to where the full notice is found.
One line to give the program's name and a brief idea of what it does.
Copyright (C)
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this when it
starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes
with ABSOLUTELY NO WARRANTY; for details type 'show w'. This is free
software, and you are welcome to redistribute it under certain conditions;
type 'show c' for details.
The hypothetical commands 'show w' and 'show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may be
called something other than 'show w' and 'show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your school,
if any, to sign a "copyright disclaimer" for the program, if necessary. Here
is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
'Gnomovision' (which makes passes at compilers) written by James Hacker.
signature of Ty Coon, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General Public
License instead of this License.
"CLASSPATH" EXCEPTION TO THE GPL
Certain source files distributed by Oracle America and/or its affiliates are
subject to the following clarification and special exception to the GPL, but
only where Oracle has expressly included in the particular source file's header
the words "Oracle designates this particular file as subject to the "Classpath"
exception as provided by Oracle in the LICENSE file that accompanied this code."
Linking this library statically or dynamically with other modules is making
a combined work based on this library. Thus, the terms and conditions of
the GNU General Public License cover the whole combination.
As a special exception, the copyright holders of this library give you
permission to link this library with independent modules to produce an
executable, regardless of the license terms of these independent modules,
and to copy and distribute the resulting executable under terms of your
choice, provided that you also meet, for each linked independent module,
the terms and conditions of the license of that module. An independent
module is a module which is not derived from or based on this library. If
you modify this library, you may extend this exception to your version of
the library, but you are not obligated to do so. If you do not wish to do
so, delete this exception statement from your version.
================================================
FILE: Makefile
================================================
# Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
#
# This code is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 only, as
# published by the Free Software Foundation.
#
# This code is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
# version 2 for more details (a copy is included in the LICENSE file that
# accompanied this code).
#
# You should have received a copy of the GNU General Public License version
# 2 along with this work; if not, write to the Free Software Foundation,
# Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
# or visit www.oracle.com if you need additional information or have any
# questions.
BUILD=build
prefix=$(HOME)/.local
bindir=$(prefix)/bin
sharedir=$(prefix)/share
mandir=$(prefix)/man
LAUNCHERS=$(addprefix $(bindir)/,$(notdir $(wildcard $(BUILD)/bin/git-*)))
MANPAGES=$(addprefix $(mandir)/man1/,$(notdir $(wildcard $(BUILD)/bin/man/man1/*)))
all:
@sh gradlew
check:
@sh gradlew test
test:
@sh gradlew test
clean:
@sh gradlew clean
images:
@sh gradlew images
bots:
@sh gradlew bots
offline:
@sh gradlew :offline
reproduce:
@sh gradlew :reproduce
install: all $(LAUNCHERS) $(MANPAGES) $(sharedir)/skara
@echo "Successfully installed to $(prefix)"
uninstall:
@rm -rf $(sharedir)/skara
@rm $(LAUNCHERS)
@rm $(MANPAGES)
$(mandir)/man1/%: $(BUILD)/bin/man/man1/%
@mkdir -p $(mandir)/man1
@cp $< $@
$(sharedir)/skara: $(BUILD)/image
@mkdir -p $(sharedir)
@rm -rf $@
@cp -r $< $@
$(bindir)/%: $(BUILD)/bin/%
@mkdir -p $(bindir)
@sed 's~export JAVA_HOME=.*$$~export JAVA_HOME\=$(sharedir)\/skara~' < $< > $@
@chmod 755 $@
.PHONY: all bots check clean images install test uninstall
================================================
FILE: README.md
================================================
# OpenJDK Project Skara
The goal of Project Skara is to investigate alternative SCM and code review
options for the OpenJDK source code, including options based upon Git rather than
Mercurial, and including options hosted by third parties.
This repository contains tooling for working with OpenJDK projects and
their repositories. The following CLI tools are available as part of this
repository:
- git-jcheck - a backwards compatible Git port of [jcheck](https://openjdk.org/projects/code-tools/jcheck/)
- git-webrev - a backwards compatible Git port of [webrev](https://openjdk.org/projects/code-tools/webrev/)
- git-defpath - a backwards compatible Git port of [defpath](https://openjdk.org/projects/code-tools/defpath/)
- git-fork - fork a project on an external Git source code hosting provider to your personal space and optionally clone it
- git-sync - sync the personal fork of the project with the current state of the upstream repository
- git-pr - interact with pull requests for a project on an external Git source code hosting provider
- git-info - show OpenJDK information about commits, e.g. issue links, authors, contributors, etc.
- git-token - interact with a Git credential manager for handling personal access tokens
- git-translate - translate between [Mercurial](https://mercurial-scm.org/)
and [Git](https://git-scm.com/) hashes
- git-skara - learn about and update the Skara CLI tools
- git-trees - run a git command in a tree of repositories
- git-publish - publishes a local branch to a remote repository
- git-backport - backports a commit from another repository onto the current branch
There are also CLI tools available for importing OpenJDK
[Mercurial](https://mercurial-scm.org/) repositories into
[Git](https://git-scm.com/) repositories and vice versa:
- git-openjdk-import
- git-verify-import
- hg-openjdk-import
The following server-side tools (so called "bots") for interacting with
external Git source code hosting providers are available:
- hgbridge - continuously convert Mercurial repositories to git
- mlbridge - bridge messages between mailing lists and pull requests
- notify - send email notifications when repositories are updated
- pr - add OpenJDK workflow support for pull requests
- submit - example pull request test runner
- forward - forward commits to various repositories
- mirror - mirror repositories
- merge - merge commits between different repositories and/or branches
- test - test runner
## Building
[JDK 21](http://jdk.java.net/21/) or later and [Gradle](https://gradle.org/)
8.5 or later are required for building and will be automatically downloaded
and installed by the custom gradlew script. To build the project on macOS or
GNU/Linux x64, just run the following command from the source tree root:
```bash
$ sh gradlew
```
To build the project on Windows x64, run the following command from the source
tree root:
```bat
> gradlew
```
The extracted jlinked image will end up in the `build` directory in the source
tree root. _Note_ that the above commands will build the CLI tools, if you
also want to build the bot images run `sh gradlew images` on GNU/Linux or
`gradlew images` on Windows.
### Other operating systems and CPU architectures
If you want to build on an operating system other than GNU/Linux, macOS or
Windows _or_ if you want to build on a CPU architecture other than x64, then
ensure that you have a JDK of suitable version or later installed locally and
JAVA_HOME set to point to it. You can then run the following command from the
source tree root:
```bash
$ sh gradlew
```
The extracted jlinked image will end up in the `build` directory in the source
tree root.
### Offline builds
If you don't want the build to automatically download any dependencies, then
you must ensure that you have installed the following software locally (see
version requirements above):
- JDK
- Gradle
To create a build then run the command:
```bash
$ gradle offline
```
_Please note_ that the above command does _not_ make use of `gradlew` to avoid
downloading Gradle.
The extracted jlinked image will end up in the `build` directory in the source
tree root.
### Cross-linking
It is also supported to cross-jlink jimages to GNU/Linux, macOS and/or Windows from
any of the aforementioned operating systems. To build all applicable jimages
(including the server-side tooling), run the following command from the
source tree root:
```bash
sh gradlew images
```
### Makefile wrapper
Skara also has a very thin Makefile wrapper for contributors who prefer to build
using `make`. To build the jlinked image for the CLI tools using `make`, run:
```bash
make
```
## Installing
There are multiple ways to install the Skara CLI tools. The easiest way is to
just include `skara.gitconfig` in your global Git configuration file. You can also
install the Skara tools on your `$PATH`.
### Including skara.gitconfig
To install the Skara tools, include the `skara.gitconfig` Git configuration
file in your user-level Git configuration file. On macOS or
GNU/Linux:
```bash
$ git config --global include.path "$PWD/skara.gitconfig"
```
On Windows:
```bat
> git config --global include.path "%CD%/skara.gitconfig"
```
To check that everything works as expected, run the command `git skara help`.
### Adding to PATH
The Skara tools can also be added to `$PATH` on GNU/Linux and macOS and Git
will pick them up. You can either just extend `$PATH` with the `build/bin`
directory or you can copy the tools to a location already on `$PATH`. To extend
`$PATH` with the `build/bin` directory, run:
```bash
$ sh gradlew
$ export PATH="$PWD/build/bin:$PATH"
```
To copy the tools to a location already on `$PATH`, run:
```bash
$ make
$ make install prefix=/path/to/install/location
```
When running `make install` the default value of `prefix` is `$HOME/.local`.
If you want `git help ` (or the equivalent `man git-`
to work, you must also add the `build/bin/man` directory to `$MANPATH`.
For instance, run this from the Skara top directory to add this to your
`.bashrc` file:
```bash
echo "export MANPATH=\$MANPATH":$PWD/build/bin/man >> ~/.bashrc
```
## Testing
[JUnit](https://junit.org/junit5/) 5.8.2 or later is required to run the unit
tests. To run the tests, execute following command from the source tree root:
```bash
$ sh gradlew test
```
If you prefer to use the Makefile wrapper you can also run:
```bash
$ make test
```
The tests expect [Git](https://git-scm.com/) version 2.19.3 or later and
[Mercurial](https://mercurial-scm.org/) 4.7.2 or later to be installed on
your system.
This repository also contains a Dockerfile, `test.dockerfile`, that allows
for running the tests in a reproducible way with the proper dependencies
configured. To run the tests in this way, run the following command from the
source tree root:
```bash
$ sh gradlew reproduce
```
If you prefer to use the Makefile wrapper you can also run:
```bash
$ make reproduce
```
## Developing
There are no additional dependencies required for developing Skara if you can
already build and test it (see above for instructions). The command-line tools
and libraries supports all of GNU/Linux, macOS and Windows and can therefore be
developed on any of those operating systems. The bots primarily support macOS
and GNU/Linux and may require [Windows Subsystem for
Linux](https://en.wikipedia.org/wiki/Windows_Subsystem_for_Linux) on Windows.
Please see the sections below for instructions on setting up a particular editor
or IDE.
### IntelliJ IDEA
If you choose to use [IntelliJ IDEA](https://www.jetbrains.com/idea/) as your
IDE when working on Skara you can simply open the root folder and the project
should be automatically imported. You will need to configure a Platform SDK that
is of the appropriate version (see above). Either set this up manually, or
[build](#building) once from the terminal, which will download a suitable JDK.
Configure IntelliJ to use it at `File → Project Structure → Platform
Settings → SDKs → + → Add JDK...` and browse to the downloaded JDK found
in `/.jdk/`. For example, on macOS, select the
`/.jdk/openjdk-21_osx-x64_bin/jdk-21.jdk/Contents/Home` folder.
### Vim
If you choose to use [Vim](https://vim.org) as your editor when working on Skara then you
probably also want to utilize the Makefile wrapper. The Makefile wrapper enables
to you to run `:make` and `:make tests` in Vim.
## Wiki
Project Skara's wiki is available at .
## Issues
Issues are tracked in the [JDK Bug System](https://bugs.openjdk.org/)
under project Skara at .
## Contributing
We are more than happy to accept contributions to the Skara tooling, both via
patches sent to the Skara
[mailing list](https://mail.openjdk.org/mailman/listinfo/skara-dev) and in the
form of pull requests on [GitHub](https://github.com/openjdk/skara/pulls/).
## Members
See for the current Skara
[Reviewers](https://openjdk.org/bylaws#reviewer),
[Committers](https://openjdk.org/bylaws#committer) and
[Authors](https://openjdk.org/bylaws#author). See
for how to become an author, committer
or reviewer in an OpenJDK project.
## Discuss
Development discussions take place on the project Skara mailing list
`skara-dev@openjdk.org`, see
for instructions
on how to subscribe of if you want to read the archives. You can also reach
many project Skara developers in the `#openjdk` IRC channel on
[OFTC](https://www.oftc.net/), see for details.
## License
See the file `LICENSE` for details.
================================================
FILE: Unzip.java
================================================
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Files;
import java.util.zip.ZipInputStream;
public class Unzip {
private static void unzip(Path zipFile, Path dest) throws IOException {
var stream = new ZipInputStream(Files.newInputStream(zipFile));
for (var entry = stream.getNextEntry(); entry != null; entry = stream.getNextEntry()) {
var path = dest.resolve(entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(path);
} else {
if (Files.exists(path)) {
Files.delete(path);
}
Files.copy(stream, path);
}
}
}
public static void main(String[] args) throws IOException {
unzip(Path.of(args[0]), Path.of(args[1]));
}
}
================================================
FILE: args/build.gradle
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module {
name = 'org.openjdk.skara.args'
test {
requires 'org.junit.jupiter.api'
opens 'org.openjdk.skara.args' to 'org.junit.platform.commons'
}
}
publishing {
publications {
args(MavenPublication) {
from components.java
}
}
}
================================================
FILE: args/src/main/java/module-info.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module org.openjdk.skara.args {
exports org.openjdk.skara.args;
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/Argument.java
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
import java.util.NoSuchElementException;
import java.util.function.*;
public class Argument {
private final String value;
public Argument() {
this.value = null;
}
public Argument(String value) {
this.value = value;
}
public boolean isPresent() {
return value != null;
}
public T via(Function f) {
if (!isPresent()) {
throw new NoSuchElementException();
}
return f.apply(value);
}
public int asInt() {
return via(Integer::parseInt);
}
public double asDouble() {
return via(Double::parseDouble);
}
public float asFloat() {
return via(Float::parseFloat);
}
public boolean asBoolean() {
return via(Boolean::parseBoolean);
}
public String asString() {
return value == null ? null : via(Function.identity());
}
public Argument or(int value) {
return isPresent() ? this : new Argument(Integer.toString(value));
}
public Argument or(double value) {
return isPresent() ? this : new Argument(Double.toString(value));
}
public Argument or(long value) {
return isPresent() ? this : new Argument(Long.toString(value));
}
public Argument or(boolean value) {
return isPresent() ? this : new Argument(Boolean.toString(value));
}
public Argument or(float value) {
return isPresent() ? this : new Argument(Float.toString(value));
}
public Argument or(String value) {
return isPresent() ? this : new Argument(value);
}
public Argument or(Argument other) {
return isPresent() ? this : other;
}
public Argument or(Supplier supplier) {
return isPresent() ? this : new Argument(supplier.get());
}
public int orInt(int value) {
return orInt(() -> value);
}
public int orInt(Supplier supplier) {
return isPresent() ? asInt() : supplier.get().intValue();
}
public double orDouble(double value) {
return orDouble(() -> value);
}
public double orDouble(Supplier supplier) {
return isPresent() ? asDouble() : supplier.get().doubleValue();
}
public float orFloat(float value) {
return orFloat(() -> value);
}
public float orFloat(Supplier supplier) {
return isPresent() ? asFloat() : supplier.get().floatValue();
}
public boolean orBoolean(boolean value) {
return orBoolean(() -> value);
}
public boolean orBoolean(Supplier supplier) {
return isPresent() ? asBoolean() : supplier.get().booleanValue();
}
public String orString(String value) {
return orString(() -> value);
}
public String orString(Supplier supplier) {
return isPresent() ? asString() : supplier.get();
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/ArgumentParser.java
================================================
/*
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
import java.io.PrintStream;
import java.util.*;
import java.util.function.Function;
public class ArgumentParser {
private final String programName;
private final List flags;
private final List inputs;
private final Map names = new HashMap<>();
private final boolean shouldShowHelp;
public ArgumentParser(String programName, List flags) {
this(programName, flags, List.of());
}
public ArgumentParser(String programName, List flags, List inputs) {
this.programName = programName;
this.flags = new ArrayList<>(flags);
this.inputs = inputs;
if (!flags.stream().anyMatch(f -> f.shortcut().equals("h") && f.fullname().equals("help"))) {
var help = Switch.shortcut("h")
.fullname("help")
.helptext("Show this help text")
.optional();
this.flags.add(help);
shouldShowHelp = true;
} else {
shouldShowHelp = false;
}
for (var flag : this.flags) {
if (!flag.fullname().equals("")) {
names.put(flag.fullname(), flag);
}
if (!flag.shortcut().equals("")) {
names.put(flag.shortcut(), flag);
}
}
}
private Flag lookupFlag(String name, boolean isShortcut) {
if (!names.containsKey(name)) {
System.err.print("Unexpected option: ");
System.err.print(isShortcut ? "-" : "--");
System.err.println(name);
showUsage();
System.exit(1);
}
return names.get(name);
}
private Flag lookupFullname(String name) {
return lookupFlag(name, false);
}
private Flag lookupShortcut(String name) {
return lookupFlag(name, true);
}
private static int longest(List flags, Function getName) {
return flags.stream()
.map(getName)
.filter(Objects::nonNull)
.mapToInt(String::length)
.reduce(0, Integer::max);
}
private static int longestShortcut(List flags) {
return longest(flags, Flag::shortcut);
}
private static int longestFullname(List flags) {
return longest(flags, f -> f.fullname() + " " + f.description());
}
public void showUsage() {
showUsage(System.out);
}
public static void showFlags(PrintStream ps, List flags, String prefix) {
var shortcutPad = longestShortcut(flags) + 1 + 2; // +1 for '-' and +2 for ', '
var fullnamePad = longestFullname(flags) + 2 + 2; // +2 for '--' and +2 for ' '
for (var flag : flags) {
ps.print(prefix);
var fmt = "%-" + shortcutPad + "s";
var s = flag.shortcut().equals("") ? " " : "-" + flag.shortcut() + ", ";
ps.print(String.format(fmt, s));
fmt = "%-" + fullnamePad + "s";
var desc = flag.description().equals("") ? "" : " " + flag.description();
s = flag.fullname().equals("") ? " " : "--" + flag.fullname() + desc + " ";
ps.print(String.format(fmt, s));
if (!flag.helptext().equals("")) {
ps.print(flag.helptext());
}
ps.println("");
}
}
public void showUsage(PrintStream ps) {
ps.print("usage: ");
ps.print(programName);
ps.print(" [options]");
for (var flag : flags) {
if (flag.isRequired()) {
ps.print(" ");
if (!flag.fullname().equals("")) {
ps.print("--");
ps.print(flag.fullname());
if (!flag.description().equals("")) {
ps.print("=");
ps.print(flag.description());
}
} else {
ps.print("-" + flag.shortcut());
if (!flag.description().equals("")) {
ps.print(" ");
ps.print(flag.description());
}
}
}
}
for (var input : inputs) {
ps.print(" ");
ps.print(input.toString());
}
ps.println("");
showFlags(ps, flags, "\t");
}
public Arguments parse(String[] args) {
var seen = new HashSet();
var values = new ArrayList();
var positional = new ArrayList();
var i = 0;
while (i < args.length) {
var arg = args[i];
if (arg.startsWith("--")) {
if (arg.contains("=")) {
var parts = arg.split("=");
var name = parts[0].substring(2); // remove leading '--'
var value = parts.length == 2 ? parts[1] : null;
var flag = lookupFullname(name);
values.add(new FlagValue(flag, value));
seen.add(flag);
} else {
var name = arg.substring(2);
var flag = lookupFullname(name);
if (flag.isSwitch()) {
values.add(new FlagValue(flag, "true"));
} else {
if (i < (args.length - 1)) {
var value = args[i + 1];
values.add(new FlagValue(flag, value));
i++;
} else {
values.add(new FlagValue(flag));
}
}
seen.add(flag);
}
} else if (arg.startsWith("-") && !arg.equals("-")) {
var name = arg.substring(1);
var flag = lookupShortcut(name);
if (flag.isSwitch()) {
values.add(new FlagValue(flag, "true"));
} else {
if (i < (args.length - 1)) {
var value = args[i + 1];
values.add(new FlagValue(flag, value));
i++;
} else {
values.add(new FlagValue(flag));
}
}
seen.add(flag);
} else {
int argPos = positional.size();
if (argPos >= inputs.size()) {
// must check if permitted
if (inputs.size() == 0) {
System.err.println("error: unexpected input: " + arg);
showUsage();
System.exit(1);
}
var last = inputs.getLast();
if ((last.getPosition() + last.getOccurrences()) <= argPos && !last.isTrailing()) {
// this input is not permitted
System.err.println("error: unexpected input: " + arg);
showUsage();
System.exit(1);
}
}
positional.add(arg);
}
i++;
}
var arguments = new Arguments(values, positional);
if (arguments.contains("help") && shouldShowHelp) {
showUsage();
System.exit(0);
}
var errors = new ArrayList();
for (var flag : flags) {
if (flag.isRequired() && !seen.contains(flag)) {
errors.add("error: missing required flag: " + flag.toString());
}
}
for (var input : inputs) {
if (input.isRequired() && !(positional.size() > input.getPosition())) {
errors.add("error: missing required input: " + input.toString());
}
}
// If --version is specified then don't care about required flags or inputs
var showVersion = arguments.contains("version");
if (!errors.isEmpty() && !showVersion) {
for (var error : errors) {
System.err.println(error);
}
showUsage();
System.exit(1);
}
return arguments;
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/Arguments.java
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
import java.util.*;
import java.util.stream.Collectors;
public class Arguments {
private final List positionals;
private final Map names = new HashMap<>();
public Arguments(List flags, List positionals) {
this.positionals = positionals;
for (var flag : flags) {
if (flag.fullname() != null) {
names.put(flag.fullname(), flag);
}
if (flag.shortcut() != null) {
names.put(flag.shortcut(), flag);
}
}
}
public List inputs() {
return positionals.stream()
.map(Argument::new)
.collect(Collectors.toList());
}
public Argument at(int pos) {
if (pos < positionals.size()) {
return new Argument(positionals.get(pos));
} else {
return new Argument();
}
}
public Argument get(String name) {
if (names.containsKey(name)) {
return new Argument(names.get(name).value());
}
return new Argument();
}
public boolean contains(String name) {
return names.containsKey(name);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/Command.java
================================================
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class Command implements Main {
private final String name;
private final String helpText;
private final Main main;
Command(String name, String helpText, Main main) {
this.name = name;
this.helpText = helpText;
this.main = main;
}
public String name() {
return name;
}
public String helpText() {
return helpText;
}
public Main main() {
return main;
}
public static CommandHelpText name(String name) {
return new CommandHelpText<>(Command::new, name);
}
@Override
public void main(String[] args) throws Exception {
main.main(args);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/CommandCtor.java
================================================
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public interface CommandCtor {
T construct(String name, String helpText, Main main);
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/CommandHelpText.java
================================================
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class CommandHelpText {
private final CommandCtor ctor;
private final String name;
CommandHelpText(CommandCtor ctor, String name) {
this.ctor = ctor;
this.name = name;
}
public CommandMain helptext(String helpText) {
return new CommandMain<>(ctor, name, helpText);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/CommandMain.java
================================================
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class CommandMain {
private final CommandCtor ctor;
private final String name;
private final String helpText;
CommandMain(CommandCtor ctor, String name, String helpText) {
this.ctor = ctor;
this.name = name;
this.helpText = helpText;
}
public T main(Main main) {
return ctor.construct(name, helpText, main);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/Default.java
================================================
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class Default extends Command {
Default(String name, String helpText, Main main) {
super(name, helpText, main);
}
public static CommandHelpText name(String name) {
return new CommandHelpText<>(Default::new, name);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/Executable.java
================================================
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
@FunctionalInterface
public interface Executable {
void execute() throws Exception;
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/Flag.java
================================================
/*
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
import java.util.Objects;
public class Flag {
private boolean isSwitch;
private final String shortcut;
private final String fullname;
private final String description;
private final String helptext;
private final boolean isRequired;
Flag(boolean isSwitch, String shortcut, String fullname, String description, String helptext, boolean isRequired) {
this.isSwitch = isSwitch;
this.shortcut = shortcut;
this.fullname = fullname;
this.description = description;
this.helptext = helptext;
this.isRequired = isRequired;
}
boolean isSwitch() {
return isSwitch;
}
public String fullname() {
return fullname;
}
public String shortcut() {
return shortcut;
}
public String description() {
return description;
}
public String helptext() {
return helptext;
}
boolean isRequired() {
return isRequired;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Flag other)) {
return false;
}
return Objects.equals(isSwitch, other.isSwitch) &&
Objects.equals(shortcut, other.shortcut) &&
Objects.equals(fullname, other.fullname) &&
Objects.equals(helptext, other.helptext) &&
Objects.equals(isRequired, other.isRequired);
}
@Override
public int hashCode() {
return Objects.hash(isSwitch,
shortcut,
fullname,
helptext,
isRequired);
}
@Override
public String toString() {
if (shortcut.equals("")) {
return "--" + fullname;
}
if (fullname.equals("")) {
return "-" + shortcut;
}
return "-" + shortcut + ", --" + fullname;
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/FlagValue.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
class FlagValue {
private final Flag flag;
private final String value;
FlagValue(Flag flag) {
this.flag = flag;
this.value = null;
}
FlagValue(Flag flag, String value) {
this.flag = flag;
this.value = value;
}
boolean isSwitch() {
return flag.isSwitch();
}
String fullname() {
return flag.fullname();
}
String shortcut() {
return flag.shortcut();
}
String helptext() {
return flag.helptext();
}
boolean isRequired() {
return flag.isRequired();
}
String value() {
return value;
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/Input.java
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class Input {
private final int position;
private final String description;
private final int occurrences;
private final boolean required;
Input(int position, String description, int occurrences, boolean required) {
this.position = position;
this.description = description;
this.occurrences = occurrences;
this.required = required;
}
public static InputDescriber position(int p) {
return new InputDescriber(p);
}
public int getPosition() {
return position;
}
public String getDescription() {
return description;
}
public int getOccurrences() {
return occurrences;
}
public boolean isTrailing() {
return occurrences == -1;
}
public boolean isRequired() {
return required;
}
@Override
public String toString() {
var builder = new StringBuilder();
var n = isTrailing() ? 1 : occurrences;
for (var i = 0; i < n; i++) {
if (!isRequired()) {
builder.append("[");
}
builder.append("<");
builder.append(description);
builder.append(">");
if (!isRequired()) {
builder.append("]");
}
if (i != (n - 1)) {
builder.append(" ");
}
if (isTrailing()) {
builder.append("...");
}
}
return builder.toString();
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/InputDescriber.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class InputDescriber {
private final int position;
InputDescriber(int position) {
this.position = position;
}
public InputQuantifier describe(String description) {
return new InputQuantifier(position, description);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/InputQualifier.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class InputQualifier {
private final int position;
private final String description;
private final int occurrences;
InputQualifier(int position, String description, int occurrences) {
this.position = position;
this.description = description;
this.occurrences = occurrences;
}
public Input optional() {
return new Input(position, description, occurrences, false);
}
public Input required() {
return new Input(position, description, occurrences, true);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/InputQuantifier.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class InputQuantifier {
private final int position;
private final String description;
InputQuantifier(int position, String description) {
this.position = position;
this.description = description;
}
public InputQualifier singular() {
return new InputQualifier(position, description, 1);
}
public InputQualifier multiple(int n) {
if (n < 1) {
throw new IllegalArgumentException(n + " must be larger than 1");
}
return new InputQualifier(position, description, n);
}
public InputQualifier trailing() {
return new InputQualifier(position, description, -1);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/Main.java
================================================
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
@FunctionalInterface
public interface Main {
void main(String[] args) throws Exception;
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/MultiCommandParser.java
================================================
/*
* Copyright (c) 2019, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class MultiCommandParser {
private final String programName;
private final String defaultCommand;
private final Map subCommands;
private final boolean defaultCommandWarningEnabled;
public MultiCommandParser(String programName, List commands, boolean defaultCommandWarningEnabled) {
var defaults = commands.stream().filter(Default.class::isInstance).collect(Collectors.toList());
if (defaults.size() != 1) {
throw new IllegalArgumentException("Expecting exactly one default command");
}
this.defaultCommand = defaults.get(0).name();
this.programName = programName;
this.subCommands = commands.stream()
.collect(Collectors.toMap(
Command::name,
Function.identity()));
this.defaultCommandWarningEnabled = defaultCommandWarningEnabled;
if (!commands.stream().anyMatch(c -> c.name().equals("help"))) {
this.subCommands.put("help", helpCommand());
}
}
private Command helpCommand() {
return new Command("help", "print a help message", args -> showUsage());
}
public Executable parse(String[] args) {
if (args.length > 0) {
var p = subCommands.get(args[0]);
if (p != null) {
var forwardedArgs = Arrays.copyOfRange(args, 1, args.length);
return () -> p.main(forwardedArgs);
}
if (defaultCommandWarningEnabled) {
System.err.println("warning: unknown sub-command: " + args[0]);
System.err.println("the default sub-command '" + defaultCommand +
"' will be executed with the arguments " + Arrays.toString(args) + "\n");
}
}
return () -> subCommands.get(defaultCommand).main(args);
}
private void showUsage() {
showUsage(System.out);
}
private void showUsage(PrintStream ps) {
ps.print("usage: ");
ps.print(programName);
ps.print(subCommands.keySet().stream().collect(Collectors.joining("|", " <", ">")));
ps.println(" ");
int spacing = subCommands.keySet().stream().mapToInt(String::length).max().orElse(0);
spacing += 8; // some room
for (var subCommand : subCommands.values()) {
ps.println(String.format(" %-" + spacing + "s%s", subCommand.name(), subCommand.helpText()));
}
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/Option.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class Option {
public static OptionFullname shortcut(String s) {
return new OptionFullname(s);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/OptionDescribe.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class OptionDescribe {
private final String shortcut;
private final String fullname;
OptionDescribe(String shortcut, String fullname) {
this.shortcut = shortcut;
this.fullname = fullname;
}
public OptionHelptext describe(String desc) {
return new OptionHelptext(shortcut, fullname, desc);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/OptionFullname.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class OptionFullname {
private final String shortcut;
OptionFullname(String shortcut) {
this.shortcut = shortcut;
}
public OptionDescribe fullname(String name) {
return new OptionDescribe(shortcut, name);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/OptionHelptext.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class OptionHelptext {
private final String shortcut;
private final String fullname;
private final String description;
OptionHelptext(String shortcut, String fullname, String description) {
this.shortcut = shortcut;
this.fullname = fullname;
this.description = description;
}
public OptionQualifier helptext(String help) {
return new OptionQualifier(shortcut, fullname, description, help);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/OptionQualifier.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class OptionQualifier {
private final String shortcut;
private final String fullname;
private final String description;
private final String helptext;
OptionQualifier(String shortcut, String fullname, String description, String helptext) {
this.shortcut = shortcut;
this.fullname = fullname;
this.description = description;
this.helptext = helptext;
}
public Flag required() {
return new Flag(false, shortcut, fullname, description, helptext, true);
}
public Flag optional() {
return new Flag(false, shortcut, fullname, description, helptext, false);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/Switch.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class Switch {
public static SwitchFullname shortcut(String s) {
return new SwitchFullname(s);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/SwitchFullname.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class SwitchFullname {
private final String shortcut;
SwitchFullname(String shortcut) {
this.shortcut = shortcut;
}
public SwitchHelptext fullname(String name) {
return new SwitchHelptext(shortcut, name);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/SwitchHelptext.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class SwitchHelptext {
private final String shortcut;
private final String fullname;
SwitchHelptext(String shortcut, String fullname) {
this.shortcut = shortcut;
this.fullname = fullname;
}
public SwitchQualifier helptext(String help) {
return new SwitchQualifier(shortcut, fullname, help);
}
}
================================================
FILE: args/src/main/java/org/openjdk/skara/args/SwitchQualifier.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
public class SwitchQualifier {
private final String shortcut;
private final String fullname;
private final String helptext;
SwitchQualifier(String shortcut, String fullname, String helptext) {
this.shortcut = shortcut;
this.fullname = fullname;
this.helptext = helptext;
}
public Flag required() {
return new Flag(true, shortcut, fullname, "", helptext, true);
}
public Flag optional() {
return new Flag(true, shortcut, fullname, "", helptext, false);
}
}
================================================
FILE: args/src/test/java/org/openjdk/skara/args/InputTests.java
================================================
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class InputTests {
@Test
void trailingToString() {
var i = Input.position(0)
.describe("ARG")
.trailing()
.required();
assertEquals("...", i.toString());
}
@Test
void singleToString() {
var i = Input.position(0)
.describe("ARG")
.singular()
.required();
assertEquals("", i.toString());
}
@Test
void multipleToString() {
var i = Input.position(0)
.describe("ARG")
.multiple(2)
.required();
assertEquals("", i.toString());
}
@Test
void optionalToString() {
var i = Input.position(0)
.describe("ARG")
.singular()
.optional();
assertEquals("[]", i.toString());
}
}
================================================
FILE: args/src/test/java/org/openjdk/skara/args/SwitchTests.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.args;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class SwitchTests {
@Test
void testFlagDescIsSwitch() {
var f = Switch.shortcut("s")
.fullname("switch")
.helptext("This is a switch")
.optional();
assertTrue(f.isSwitch());
}
}
================================================
FILE: bot/build.gradle
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module {
name = 'org.openjdk.skara.bot'
test {
requires 'org.junit.jupiter.api'
opens 'org.openjdk.skara.bot' to 'org.junit.platform.commons'
}
}
dependencies {
implementation project(':ci')
implementation project(':host')
implementation project(':network')
implementation project(':issuetracker')
implementation project(':forge')
implementation project(':vcs')
implementation project(':json')
implementation project(':census')
implementation project(':metrics')
implementation project(':version')
}
publishing {
publications {
bot(MavenPublication) {
from components.java
}
}
}
================================================
FILE: bot/src/main/java/module-info.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module org.openjdk.skara.bot {
requires transitive org.openjdk.skara.ci;
requires transitive org.openjdk.skara.host;
requires transitive org.openjdk.skara.issuetracker;
requires transitive org.openjdk.skara.forge;
requires transitive org.openjdk.skara.json;
requires transitive org.openjdk.skara.census;
requires transitive org.openjdk.skara.metrics;
requires org.openjdk.skara.network;
requires org.openjdk.skara.vcs;
requires org.openjdk.skara.version;
requires java.logging;
requires java.management;
requires jdk.management;
requires jdk.httpserver;
requires jdk.jfr;
exports org.openjdk.skara.bot;
uses org.openjdk.skara.bot.BotFactory;
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/Bot.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import org.openjdk.skara.json.JSONValue;
import java.util.List;
public interface Bot {
List getPeriodicItems();
default List processWebHook(JSONValue body) {
return List.of();
};
String name();
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/BotConfiguration.java
================================================
/*
* Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import org.openjdk.skara.ci.ContinuousIntegration;
import org.openjdk.skara.forge.HostedRepository;
import org.openjdk.skara.issuetracker.IssueProject;
import org.openjdk.skara.issuetracker.IssueTracker;
import org.openjdk.skara.json.JSONObject;
import java.nio.file.Path;
public interface BotConfiguration {
/**
* Folder that WorkItems may use to store permanent data.
* @return
*/
Path storageFolder();
/**
* Configuration-specific name mapped to a HostedRepository.
* @param name
* @return
*/
HostedRepository repository(String name);
/**
* Configuration-specific name mapped to an IssueProject.
* @param name
* @return
*/
IssueProject issueProject(String name);
/**
* Configuration-specific name mapped to an IssueTracker.
* @param name
* @return
*/
IssueTracker issueTracker(String name);
/**
* Configuration-specific name mapped to a ContinuousIntegration.
* @param name
* @return
*/
ContinuousIntegration continuousIntegration(String name);
/**
* Retrieves the ref name that optionally follows the configuration-specific repository name.
* If not configured, returns the name of the VCS default branch.
* @param name
* @return
*/
String repositoryRef(String name);
/**
* Extracts a reasonable short repository name from a full repository specification, e.g. host/org/repo:ref -> repo
* @param name
* @return
*/
String repositoryName(String name);
/**
* Additional bot-specific configuration.
* @return
*/
JSONObject specific();
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/BotFactory.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import java.util.*;
import java.util.stream.*;
public interface BotFactory {
/**
* A user-friendly name for the given bot, used for configuration section naming. Should be lower case.
* @return
*/
String name();
/**
* Instantiate instances of this bot with the given configuration.
* @param configuration
* @return
*/
List create(BotConfiguration configuration);
static List getBotFactories() {
return StreamSupport.stream(ServiceLoader.load(BotFactory.class).spliterator(), false)
.collect(Collectors.toList());
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/BotRunner.java
================================================
/*
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import java.util.concurrent.atomic.AtomicInteger;
import org.openjdk.skara.json.JSONValue;
import org.openjdk.skara.metrics.*;
import java.io.IOException;
import java.nio.file.Path;
import java.net.*;
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.logging.*;
import java.lang.management.ManagementFactory;
import com.sun.management.ThreadMXBean;
import com.sun.net.httpserver.*;
import org.openjdk.skara.network.RestRequest;
import org.openjdk.skara.network.UncheckedRestException;
class BotRunnerError extends RuntimeException {
BotRunnerError(String msg) {
super(msg);
}
BotRunnerError(String msg, Throwable suppressed) {
super(msg);
addSuppressed(suppressed);
}
}
public class BotRunner {
enum TaskPhases {
BEGIN,
END
}
private final AtomicInteger workIdCounter = new AtomicInteger();
/**
* A wrapper for a WorkItem while it's tracked as pending. Used to track
* when a particular WorkItem entered the pending state so that metrics
* and log messages can use this information.
*/
private static class PendingWorkItem {
private final WorkItem item;
private final Instant createTime;
public PendingWorkItem(WorkItem item) {
this(item, null);
}
public PendingWorkItem(WorkItem item, Instant originalCreateTime) {
this.item = item;
if (originalCreateTime != null) {
this.createTime = originalCreateTime;
} else {
this.createTime = Instant.now();
}
}
}
private class RunnableWorkItem implements Runnable {
private static final Counter.WithThreeLabels EXCEPTIONS_COUNTER =
Counter.name("skara_runner_exceptions").labels("bot", "work_item", "exception").register();
/**
* Gauge that tracks the time WorkItems have been pending before
* being submitted.
*/
private static final Gauge.WithTwoLabels PENDING_TIME_GAUGE =
Gauge.name("skara_runner_pending_time").labels("bot", "work_item").register();
/**
* Gauge that tracks the time WorkItems have been submitted before
* starting to run.
*/
private static final Gauge.WithTwoLabels SUBMITTED_TIME_GAUGE =
Gauge.name("skara_runner_submitted_time").labels("bot", "work_item").register();
private static final Counter.WithTwoLabels TIME_COUNTER =
Counter.name("skara_runner_run_time_total").labels("bot", "work_item").register();
private static final Counter.WithTwoLabels ITEM_FINISHED_COUNTER =
Counter.name("skara_runner_finished_counter").labels("bot", "work_item").register();
private static final Counter.WithTwoLabels CPU_TIME_COUNTER =
Counter.name("skara_runner_cpu_time_total").labels("bot", "work_item").register();
private static final Counter.WithTwoLabels ALLOCATED_BYTES_COUNTER =
Counter.name("skara_runner_allocated_bytes_total").labels("bot", "work_item").register();
private final WorkItem item;
private final int workId = workIdCounter.incrementAndGet();
private final Instant createTime = Instant.now();
// This gets updated by the watchdog when a timeout occurs to avoid
// repeating the timeout log messages too often.
private Instant timeoutWarningTime = createTime;
RunnableWorkItem(WorkItem wrappedItem) {
item = wrappedItem;
}
public WorkItem get() {
return item;
}
private static Optional getThreadMXBean() {
var bean = ManagementFactory.getThreadMXBean();
return bean instanceof ThreadMXBean b ?
Optional.of(b) : Optional.empty();
}
private static void enableThreadCpuTime() {
var bean = getThreadMXBean();
if (bean.get().isCurrentThreadCpuTimeSupported() && !bean.get().isThreadCpuTimeEnabled()) {
bean.get().setThreadCpuTimeEnabled(true);
}
}
private static long getCurrentThreadCpuTime() {
var bean = getThreadMXBean();
if (bean.isEmpty()) {
return -1L;
}
return bean.get().isCurrentThreadCpuTimeSupported()?
bean.get().getCurrentThreadCpuTime() :
-1L;
}
private static long getCurrentThreadAllocatedBytes() {
var bean = getThreadMXBean();
if (bean.isEmpty()) {
return -1L;
}
if (!bean.get().isThreadAllocatedMemorySupported()) {
return -1L;
}
if (!bean.get().isThreadAllocatedMemoryEnabled()) {
bean.get().setThreadAllocatedMemoryEnabled(true);
}
return bean.get().getCurrentThreadAllocatedBytes();
}
@Override
public void run() {
enableThreadCpuTime();
long startCpuTimeNs = getCurrentThreadCpuTime();
long startAllocatedBytes = getCurrentThreadAllocatedBytes();
var start = Instant.now();
try {
runMeasured();
} finally {
ITEM_FINISHED_COUNTER.labels(item.botName(), item.workItemName()).inc();
long stopCpuTimeNs = getCurrentThreadCpuTime();
long stopAllocatedBytes = getCurrentThreadAllocatedBytes();
var cpuTimeNs = (startCpuTimeNs == -1L && stopCpuTimeNs == -1L)?
-1L : stopCpuTimeNs - startCpuTimeNs;
var allocatedBytes = (startAllocatedBytes == -1L && stopAllocatedBytes == -1L)?
-1L : stopAllocatedBytes - startAllocatedBytes;
if (cpuTimeNs != -1L) {
double cpuTimeSeconds = cpuTimeNs / 1_000_000_000.0;
CPU_TIME_COUNTER.labels(item.botName(), item.workItemName()).inc(cpuTimeSeconds);
}
if (allocatedBytes != -1L) {
ALLOCATED_BYTES_COUNTER.labels(item.botName(), item.workItemName()).inc(allocatedBytes);
}
TIME_COUNTER.labels(item.botName(), item.workItemName()).inc(
Duration.between(start, Instant.now()).toMillis() / 1_000.0);
}
}
private void runMeasured() {
Path scratchPath;
synchronized (executor) {
if (scratchPaths.isEmpty()) {
log.warning("No scratch paths available - postponing " + item);
addPending(new PendingWorkItem(item), null);
return;
}
scratchPath = scratchPaths.removeFirst();
}
Collection followUpItems = null;
var start = Instant.now();
try (var __ = new LogContext(Map.of("work_item", item.toString(),
"work_id", String.valueOf(workId)))) {
var submittedDuration = Duration.between(createTime, start);
SUBMITTED_TIME_GAUGE.labels(item.botName(), item.workItemName()).set(submittedDuration.toMillis() / 1_000.0);
log.log(Level.FINE, "Executing item " + item + " on repository " + scratchPath
+ " after being submitted for " + submittedDuration,
new Object[]{TaskPhases.BEGIN, submittedDuration});
try {
followUpItems = item.run(scratchPath);
} catch (UncheckedRestException e) {
EXCEPTIONS_COUNTER.labels(item.botName(), item.workItemName(), e.getClass().getName()).inc();
// Log as WARNING to avoid triggering alarms. Failed REST calls are tracked
// using metrics.
log.log(Level.WARNING, "RestException during item execution (" + item + "): "
+ e.getMessage(), e);
item.handleRuntimeException(e);
} catch (RuntimeException e) {
EXCEPTIONS_COUNTER.labels(item.botName(), item.workItemName(), e.getClass().getName()).inc();
if (e.getCause() instanceof UncheckedRestException) {
// Log as WARNING to avoid triggering alarms. Failed REST calls are tracked
// using metrics.
log.log(Level.WARNING, "RestException during item execution (" + item + ")"
+ e.getCause().getMessage(), e.getCause());
} else {
log.log(Level.SEVERE, "Exception during item execution (" + item + "): " + e.getMessage(), e);
}
item.handleRuntimeException(e);
} catch (Error e) {
EXCEPTIONS_COUNTER.labels(item.botName(), item.workItemName(), e.getClass().getName()).inc();
log.log(Level.SEVERE, "Error thrown during item execution: (" + item + "): " + e.getMessage(), e);
throw e;
} finally {
var duration = Duration.between(start, Instant.now());
log.log(Level.FINE, "Item " + item + " is now done after " + duration,
new Object[]{TaskPhases.END, duration});
synchronized (executor) {
scratchPaths.addLast(scratchPath);
done(item);
}
}
if (followUpItems != null) {
followUpItems.forEach(BotRunner.this::submitOrSchedule);
}
synchronized (executor) {
// Some of the pending items may now be eligible for execution
var candidateItems = pending.entrySet().stream()
.filter(e -> e.getValue().isEmpty() || !active.containsKey(e.getValue().get()))
.map(Map.Entry::getKey)
.toList();
// Try the candidates against the current active set
for (var candidate : candidateItems) {
boolean maySubmit = true;
for (var activeItem : active.keySet()) {
if (!activeItem.concurrentWith(candidate.item)) {
// Still can't run this candidate, leave it pending
log.finer("Cannot submit candidate " + candidate + " - not concurrent with " + activeItem);
maySubmit = false;
break;
}
}
if (maySubmit) {
removePending(candidate);
submit(candidate.item);
var timeSinceCreation = Duration.between(candidate.createTime, Instant.now());
PENDING_TIME_GAUGE.labels(candidate.item.botName(), candidate.item.workItemName())
.set(timeSinceCreation.toMillis() / 1_000.0);
log.log(Level.FINE, "Submitting item " + candidate.item
+ " after being pending for " + timeSinceCreation, timeSinceCreation);
}
}
}
}
}
}
// Mapping of pending items to the active item preventing them from running
private final Map> pending;
// Mapping of active WorkItem to their RunnableWorkItem
private final Map active;
private final Deque scratchPaths;
private static final Counter.WithTwoLabels SCHEDULED_COUNTER =
Counter.name("skara_runner_scheduled_counter").labels("bot", "work_item").register();
private static final Counter.WithTwoLabels PENDING_COUNTER =
Counter.name("skara_runner_pending_counter").labels("bot", "work_item").register();
private static final Counter.WithTwoLabels SUBMITTED_COUNTER =
Counter.name("skara_runner_submitted_counter").labels("bot", "work_item").register();
private static final Counter.WithTwoLabels DISCARDED_COUNTER =
Counter.name("skara_runner_discarded_counter").labels("bot", "work_item").register();
/**
* Gauge that tracks the number of active WorkItems for each kind
*/
private static final Gauge.WithTwoLabels ACTIVE_GAUGE =
Gauge.name("skara_runner_active").labels("bot", "work_item").register();
/**
* Gauge that tracks the number of pending WorkItems for each kind
*/
private static final Gauge.WithTwoLabels PENDING_GAUGE =
Gauge.name("skara_runner_pending").labels("bot", "work_item").register();
private void submitOrSchedule(WorkItem item) {
SCHEDULED_COUNTER.labels(item.botName(), item.workItemName()).inc();
synchronized (executor) {
for (var activeItem : active.keySet()) {
if (!activeItem.concurrentWith(item)) {
Instant originalCreateTime = null;
for (var pendingItem : pending.keySet()) {
// If there are pending items of the same type that we cannot run concurrently with, replace them.
if (item.replaces(pendingItem.item)) {
log.finer("Discarding obsoleted item " + pendingItem +
" in favor of item " + item);
DISCARDED_COUNTER.labels(item.botName(), item.workItemName()).inc();
removePending(pendingItem);
originalCreateTime = pendingItem.createTime;
// There can't be more than one
break;
}
}
log.fine("Adding pending item " + item);
addPending(new PendingWorkItem(item, originalCreateTime), activeItem);
return;
}
}
log.fine("Submitting item " + item);
submit(item);
}
}
/**
* Called to add a WorkItem to the pending queue
* @param pendingItem Item to queue
* @param activeItem Optional active item that this item is waiting for
*/
private void addPending(PendingWorkItem pendingItem, WorkItem activeItem) {
pending.put(pendingItem, Optional.ofNullable(activeItem));
PENDING_GAUGE.labels(pendingItem.item.botName(), pendingItem.item.workItemName()).inc();
PENDING_COUNTER.labels(pendingItem.item.botName(), pendingItem.item.workItemName()).inc();
}
/**
* Called to remove an item from the pending queue.
*/
private void removePending(PendingWorkItem pendingItem) {
pending.remove(pendingItem);
PENDING_GAUGE.labels(pendingItem.item.botName(), pendingItem.item.workItemName()).dec();
}
/**
* Called to submit a WorkItem for execution
*/
private void submit(WorkItem item) {
RunnableWorkItem runnableWorkItem = new RunnableWorkItem(item);
executor.submit(runnableWorkItem);
active.put(item, runnableWorkItem);
ACTIVE_GAUGE.labels(item.botName(), item.workItemName()).inc();
SUBMITTED_COUNTER.labels(item.botName(), item.workItemName()).inc();
}
/**
* Called when a WorkItem is done executing
*/
private void done(WorkItem item) {
active.remove(item);
ACTIVE_GAUGE.labels(item.botName(), item.workItemName()).dec();
}
private void drain(Duration timeout) throws TimeoutException {
Instant start = Instant.now();
while (Instant.now().isBefore(start.plus(timeout))) {
while (true) {
var head = (ScheduledFuture>) executor.getQueue().peek();
if (head != null) {
log.fine("Waiting for future to complete");
try {
head.get();
} catch (InterruptedException | ExecutionException e) {
log.log(Level.WARNING, "Exception during queue drain", e);
}
} else {
log.finest("Queue is now empty");
break;
}
}
synchronized (executor) {
if (pending.isEmpty() && active.isEmpty()) {
log.fine("Nothing awaiting scheduling - drain is finished");
return;
} else {
log.finest("Waiting for flighted tasks");
}
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
log.log(Level.WARNING, "Exception during queue drain", e);
}
}
throw new TimeoutException();
}
private final BotRunnerConfiguration config;
private final List bots;
private final ScheduledThreadPoolExecutor executor;
private final BotWatchdog botWatchdog;
private final Duration watchdogWarnTimeout;
private volatile boolean isReady;
private volatile boolean isHealthy;
private static final Logger log = Logger.getLogger("org.openjdk.skara.bot");
public BotRunner(BotRunnerConfiguration config, List bots) {
this.config = config;
this.bots = bots;
pending = new HashMap<>();
active = new HashMap<>();
scratchPaths = new LinkedList<>();
for (int i = 0; i < config.concurrency(); ++i) {
var folder = config.scratchFolder().resolve("scratch-" + i);
scratchPaths.addLast(folder);
}
executor = new ScheduledThreadPoolExecutor(config.concurrency());
botWatchdog = new BotWatchdog(config.watchdogTimeout(), () -> isHealthy = false);
watchdogWarnTimeout = config.watchdogWarnTimeout();
isReady = false;
isHealthy = true;
}
boolean isReady() {
return isReady;
}
boolean isHealthy() {
return isHealthy;
}
private static final Gauge PERIODIC_CHECK_TIME_GAUGE =
Gauge.name("skara_runner_check_time_gauge").register();
private static final Counter.WithOneLabel PERIODIC_CHECK_TIME =
Counter.name("skara_runner_check_time").labels("bot").register();
private void checkPeriodicItems() {
try (var __ = new LogContext("work_id", String.valueOf(workIdCounter.incrementAndGet()))) {
Instant start = Instant.now();
log.log(Level.FINE, "Start of checking for periodic items", TaskPhases.BEGIN);
try {
for (var bot : bots) {
Instant botStart = Instant.now();
try (var ___ = new LogContext("bot", bot.toString())) {
log.fine("Start of checking for periodic items for " + bot);
var items = bot.getPeriodicItems();
for (var item : items) {
submitOrSchedule(item);
}
} catch (UncheckedRestException e) {
// Log as WARNING to avoid triggering alarms. Failed REST calls are tracked
// using metrics.
log.log(Level.WARNING, "RestException during periodic items checking: " + e.getMessage(), e);
} catch (RuntimeException e) {
log.log(Level.SEVERE, "Exception during periodic items checking: " + e.getMessage(), e);
} finally {
var duration = Duration.between(botStart, Instant.now());
log.log(Level.FINE, "Checking for periodic items for " + bot + " took " + duration, duration);
PERIODIC_CHECK_TIME.labels(bot.name()).inc(duration.toMillis() / 1_000.0);
}
}
} finally {
var duration = Duration.between(start, Instant.now());
log.log(Level.FINE, "Checking periodic items took " + duration,
new Object[]{TaskPhases.END, duration});
PERIODIC_CHECK_TIME_GAUGE.set(duration.toMillis() / 1_000.0);
}
}
}
private void itemWatchdog() {
synchronized (executor) {
for (var activeRunnableItem : active.values()) {
Instant now = Instant.now();
var timeoutDuration = Duration.between(activeRunnableItem.timeoutWarningTime, now);
if (timeoutDuration.compareTo(watchdogWarnTimeout) > 0) {
log.severe("Item " + activeRunnableItem.item + " with workId " + activeRunnableItem.workId + " has been active more than " +
Duration.between(activeRunnableItem.createTime, now) + " - this may be an error!");
// Reset the counter to avoid continuous reporting - once every watchdogTimeout is enough
activeRunnableItem.timeoutWarningTime = now;
}
}
// Inform the global watchdog that the scheduler is still executing items
log.fine("Pinging Watchdog");
botWatchdog.ping();
}
}
void processWebhook(JSONValue request) {
try (var __ = new LogContext("work_id", String.valueOf(workIdCounter.incrementAndGet()))) {
log.log(Level.FINE, "Starting processing of incoming rest request", TaskPhases.BEGIN);
log.fine("Request: " + request);
try {
for (var bot : bots) {
var items = bot.processWebHook(request);
for (var item : items) {
submitOrSchedule(item);
}
}
} catch (RuntimeException e) {
log.log(Level.SEVERE, "Exception during rest request processing: " + e.getMessage(), e);
} finally {
log.log(Level.FINE, "Done processing incoming rest request", TaskPhases.END);
}
}
}
public void run() {
run(Duration.ofDays(10 * 365));
}
public void run(Duration timeout) {
log.info("Periodic task interval: " + config.scheduledExecutionPeriod());
log.info("Concurrency: " + config.concurrency());
HttpServer server = null;
var serverConfig = config.httpServer(this);
if (serverConfig.isPresent()) {
try {
var port = serverConfig.get().port();
var address = new InetSocketAddress(port);
server = HttpServer.create(address, 0);
server.setExecutor(null);
for (var context : serverConfig.get().contexts()) {
server.createContext(context.path(), context.handler());
}
server.start();
} catch (IOException e) {
log.log(Level.WARNING, "Failed to create HTTP server", e);
}
}
isReady = true;
var schedulingInterval = config.scheduledExecutionPeriod().toMillis();
executor.scheduleAtFixedRate(this::itemWatchdog, 0, schedulingInterval, TimeUnit.MILLISECONDS);
executor.scheduleAtFixedRate(this::checkPeriodicItems, 0, schedulingInterval, TimeUnit.MILLISECONDS);
var cacheEvictionInterval = config.cacheEvictionInterval().toMillis();
executor.scheduleAtFixedRate(RestRequest::evictOldCacheData, cacheEvictionInterval,
cacheEvictionInterval, TimeUnit.MILLISECONDS);
try {
executor.awaitTermination(timeout.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (server != null) {
server.stop(0);
}
executor.shutdown();
}
public void runOnce(Duration timeout) throws TimeoutException {
log.info("Starting BotRunner execution, will run once");
log.info("Timeout: " + timeout);
log.info("Concurrency: " + config.concurrency());
var periodics = executor.submit(this::checkPeriodicItems);
try {
log.fine("Make sure periodics execute at least once");
periodics.get();
log.fine("Periodics have now run");
} catch (InterruptedException e) {
throw new BotRunnerError("Interrupted", e);
} catch (ExecutionException e) {
throw new BotRunnerError("Execution error", e);
}
log.fine("Waiting for all spawned tasks");
drain(timeout);
log.fine("Done waiting for all tasks");
executor.shutdown();
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/BotRunnerConfiguration.java
================================================
/*
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import org.openjdk.skara.ci.ContinuousIntegration;
import org.openjdk.skara.forge.*;
import org.openjdk.skara.host.Credential;
import org.openjdk.skara.issuetracker.*;
import org.openjdk.skara.json.JSONObject;
import org.openjdk.skara.network.URIBuilder;
import org.openjdk.skara.vcs.Branch;
import org.openjdk.skara.vcs.VCS;
import java.io.*;
import java.net.URI;
import java.nio.file.*;
import java.time.Duration;
import java.util.*;
import java.util.function.BiFunction;
import java.util.logging.Logger;
import com.sun.net.httpserver.HttpHandler;
public class BotRunnerConfiguration {
private final Logger log;
private final JSONObject config;
private final Map repositoryHosts;
private final Map issueHosts;
private final Map continuousIntegrations;
private final Map repositories;
private BotRunnerConfiguration(JSONObject config, Path cwd) throws ConfigurationError {
this.config = config;
log = Logger.getLogger("org.openjdk.skara.bot");
repositoryHosts = parseRepositoryHosts(config, cwd);
issueHosts = parseIssueHosts(config, cwd);
continuousIntegrations = parseContinuousIntegrations(config, cwd);
repositories = parseRepositories(config);
}
private Map parseRepositoryHosts(JSONObject config, Path cwd) throws ConfigurationError {
Map ret = new HashMap<>();
if (!config.contains("forges")) {
return ret;
}
for (var entry : config.get("forges").fields()) {
if (entry.value().contains("gitlab")) {
var gitlab = entry.value().get("gitlab");
var uri = URIBuilder.base(gitlab.get("url").asString()).build();
var pat = new Credential(gitlab.get("username").asString(), gitlab.get("pat").asString());
ret.put(entry.name(), Forge.from("gitlab", uri, pat, gitlab.asObject()));
} else if (entry.value().contains("github")) {
var github = entry.value().get("github");
URI uri;
if (github.contains("url")) {
uri = URIBuilder.base(github.get("url").asString()).build();
} else {
uri = URIBuilder.base("https://github.com/").build();
}
if (github.contains("app")) {
var keyFile = cwd.resolve(github.get("app").get("key").asString());
try {
var keyContents = Files.readString(keyFile);
var pat = new Credential(github.get("app").get("id").asString() + ";" +
github.get("app").get("installation").asString(),
keyContents);
ret.put(entry.name(), Forge.from("github", uri, pat, github.asObject()));
} catch (IOException e) {
throw new ConfigurationError("Cannot find key file: " + keyFile);
}
} else if (github.contains("username")) {
var pat = new Credential(github.get("username").asString(), github.get("pat").asString());
ret.put(entry.name(), Forge.from("github", uri, pat, github.asObject()));
} else {
ret.put(entry.name(), Forge.from("github", uri, github.asObject()));
}
} else if (entry.value().contains("bitbucket")) {
var bitbucket = entry.value().get("bitbucket");
var uri = URIBuilder.base(bitbucket.get("url").asString()).build();
var credential = new Credential(bitbucket.get("username").asString(), bitbucket.get("pat").asString());
ret.put(entry.name(), Forge.from("bitbucket", uri, credential, bitbucket.asObject()));
} else {
throw new ConfigurationError("Host " + entry.name());
}
}
return ret;
}
private Map parseIssueHosts(JSONObject config, Path cwd) throws ConfigurationError {
Map ret = new HashMap<>();
if (!config.contains("issuetrackers")) {
return ret;
}
for (var entry : config.get("issuetrackers").fields()) {
if (entry.value().contains("jira")) {
var jira = entry.value().get("jira");
var uri = URIBuilder.base(jira.get("url").asString()).build();
Credential credential = null;
if (jira.contains("username")) {
credential = new Credential(jira.get("username").asString(), jira.get("password").asString());
}
ret.put(entry.name(), IssueTracker.from("jira", uri, credential, jira.asObject()));
} else {
throw new ConfigurationError("Host " + entry.name());
}
}
return ret;
}
private Map parseContinuousIntegrations(JSONObject config, Path cwd) throws ConfigurationError {
Map ret = new HashMap<>();
if (!config.contains("ci")) {
return ret;
}
for (var entry : config.get("ci").fields()) {
var url = entry.value().get("url").asString();
var ci = ContinuousIntegration.from(URI.create(url), entry.value().asObject());
if (ci.isPresent()) {
ret.put(entry.name(), ci.get());
} else {
throw new ConfigurationError("No continuous integration named with url: " + url);
}
}
return ret;
}
private Map parseRepositories(JSONObject config) throws ConfigurationError {
Map ret = new HashMap<>();
if (!config.contains("repositories")) {
return ret;
}
for (var entry : config.get("repositories").fields()) {
var hostName = entry.value().get("host").asString();
if (!repositoryHosts.containsKey(hostName)) {
throw new ConfigurationError("Repository " + entry.name() + " uses undefined host '" + hostName + "'");
}
var host = repositoryHosts.get(hostName);
var repo = host.repository(entry.value().get("repository").asString()).orElseThrow(() ->
new ConfigurationError("Repository " + entry.value().get("repository").asString() + " is not available at " + hostName)
);
ret.put(entry.name(), repo);
}
return ret;
}
private static class RepositoryEntry {
HostedRepository repository;
String ref;
}
private RepositoryEntry parseRepositoryEntry(String entry) throws ConfigurationError {
var ret = new RepositoryEntry();
var refSeparatorIndex = entry.indexOf(':');
if (refSeparatorIndex >= 0) {
ret.ref = entry.substring(refSeparatorIndex + 1);
entry = entry.substring(0, refSeparatorIndex);
}
var hostSeparatorIndex = entry.indexOf('/');
if (hostSeparatorIndex >= 0) {
var hostName = entry.substring(0, hostSeparatorIndex);
var host = repositoryHosts.get(hostName);
if (!repositoryHosts.containsKey(hostName)) {
throw new ConfigurationError("Repository entry " + entry + " uses undefined host '" + hostName + "'");
}
var repositoryName = entry.substring(hostSeparatorIndex + 1);
ret.repository = host.repository(repositoryName).orElseThrow(() ->
new ConfigurationError("Repository " + repositoryName + " is not available at " + hostName)
);
} else {
if (!repositories.containsKey(entry)) {
throw new ConfigurationError("Repository " + entry + " is not defined!");
}
ret.repository = repositories.get(entry);
}
if (ret.ref == null) {
ret.ref = Branch.defaultFor(ret.repository.repositoryType()).name();
}
return ret;
}
private IssueProject parseIssueProjectEntry(String entry) throws ConfigurationError {
var hostSeparatorIndex = entry.indexOf('/');
if (hostSeparatorIndex >= 0) {
var hostName = entry.substring(0, hostSeparatorIndex);
var host = issueHosts.get(hostName);
if (!issueHosts.containsKey(hostName)) {
throw new ConfigurationError("Issue project entry " + entry + " uses undefined host '" + hostName + "'");
}
var issueProjectName = entry.substring(hostSeparatorIndex + 1);
return host.project(issueProjectName);
} else {
throw new ConfigurationError("Malformed issue project entry");
}
}
public static BotRunnerConfiguration parse(JSONObject config, Path cwd) throws ConfigurationError {
return new BotRunnerConfiguration(config, cwd);
}
public static BotRunnerConfiguration parse(JSONObject config) throws ConfigurationError {
return parse(config, Paths.get("."));
}
public BotConfiguration perBotConfiguration(String botName) throws ConfigurationError {
if (!config.contains(botName)) {
throw new ConfigurationError("No configuration for bot name: " + botName);
}
return new BotConfiguration() {
@Override
public Path storageFolder() {
if (!config.contains("storage") || !config.get("storage").contains("path")) {
try {
return Files.createTempDirectory("storage-" + botName);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
return Paths.get(config.get("storage").get("path").asString()).resolve(botName);
}
@Override
public HostedRepository repository(String name) {
try {
var entry = parseRepositoryEntry(name);
return entry.repository;
} catch (ConfigurationError configurationError) {
throw new RuntimeException("Couldn't find repository with name: " + name, configurationError);
}
}
@Override
public IssueTracker issueTracker(String name) {
if (!issueHosts.containsKey(name)) {
throw new RuntimeException("Couldn't find issue tracker with name: " + name);
}
return issueHosts.get(name);
}
@Override
public IssueProject issueProject(String name) {
try {
return parseIssueProjectEntry(name);
} catch (ConfigurationError configurationError) {
throw new RuntimeException("Couldn't find issue project with name: " + name, configurationError);
}
}
@Override
public ContinuousIntegration continuousIntegration(String name) {
if (continuousIntegrations.containsKey(name)) {
return continuousIntegrations.get(name);
}
throw new RuntimeException("Couldn't find continuous integration with name: " + name);
}
@Override
public String repositoryRef(String name) {
try {
var entry = parseRepositoryEntry(name);
return entry.ref;
} catch (ConfigurationError configurationError) {
throw new RuntimeException("Couldn't find repository with name: " + name, configurationError);
}
}
@Override
public String repositoryName(String name) {
var refIndex = name.indexOf(':');
if (refIndex >= 0) {
name = name.substring(0, refIndex);
}
var orgIndex = name.lastIndexOf('/');
if (orgIndex >= 0) {
name = name.substring(orgIndex + 1);
}
return name;
}
@Override
public JSONObject specific() {
return config.get(botName).asObject();
}
};
}
/**
* The amount of time to wait between each invocation of Bot.getPeriodicItems.
* @return
*/
Duration scheduledExecutionPeriod() {
if (!config.contains("runner") || !config.get("runner").contains("interval")) {
log.info("No WorkItem invocation period defined, using default value");
return Duration.ofSeconds(10);
} else {
return Duration.parse(config.get("runner").get("interval").asString());
}
}
/**
* The amount of time to wait between runs of the RestResponseCache evictions.
* @return
*/
Duration cacheEvictionInterval() {
if (!config.contains("runner") || !config.get("runner").contains("cache_eviction_interval")) {
var defaultValue = Duration.ofMinutes(5);
log.info("No cache eviction interval defined, using default value " + defaultValue);
return defaultValue;
} else {
return Duration.parse(config.get("runner").get("cache_eviction_interval").asString());
}
}
/**
* Number of WorkItems to execute in parallel.
* @return
*/
Integer concurrency() {
if (!config.contains("runner") || !config.get("runner").contains("concurrency")) {
log.info("WorkItem concurrency not defined, using default value");
return 2;
} else {
return config.get("runner").get("concurrency").asInt();
}
}
/**
* Folder that WorkItems may use to store temporary data.
* @return
*/
Path scratchFolder() {
if (!config.contains("scratch") || !config.get("scratch").contains("path")) {
try {
log.warning("No scratch folder defined, creating a temporary folder");
return Files.createTempDirectory("botrunner");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
return Paths.get(config.get("scratch").get("path").asString());
}
static class HttpContextConfiguration {
private final String path;
private final HttpHandler handler;
private HttpContextConfiguration(String path, HttpHandler handler) {
this.path = path;
this.handler = handler;
}
String path() {
return path;
}
HttpHandler handler() {
return handler;
}
}
static class HttpServerConfiguration {
private final int port;
private final List contexts;
private HttpServerConfiguration(int port, List contexts) {
this.port = port;
this.contexts = contexts;
}
int port() {
return port;
}
List contexts() {
return contexts;
}
}
Optional httpServer(BotRunner runner) {
if (!config.contains("http-server")) {
return Optional.empty();
}
Map> factories = Map.of(
WebhookHandler.name(), WebhookHandler::create,
MetricsHandler.name(), MetricsHandler::create,
ReadinessHandler.name(), ReadinessHandler::create,
LivenessHandler.name(), LivenessHandler::create,
ProfileHandler.name(), ProfileHandler::create,
VersionHandler.name(), VersionHandler::create
);
var contexts = new ArrayList();
var port = config.get("http-server").get("port").asInt();
for (var field : config.get("http-server").fields()) {
if (field.name().startsWith("/")) {
var path = field.name();
var type = field.value().get("type").asString();
if (!factories.containsKey(type)) {
throw new RuntimeException("Unknown kind of HTTP handler: " + type);
}
var handler = factories.get(type).apply(runner, field.value().asObject());
contexts.add(new HttpContextConfiguration(path, handler));
}
}
return Optional.of(new HttpServerConfiguration(port, contexts));
}
Duration watchdogTimeout() {
if (!config.contains("runner") || !config.get("runner").contains("watchdog")) {
log.info("No WorkItem watchdog timeout defined, using default value");
return Duration.ofMinutes(30);
} else {
return Duration.parse(config.get("runner").get("watchdog").asString());
}
}
Duration watchdogWarnTimeout() {
if (!config.contains("runner") || !config.get("runner").contains("watchdog_warn")) {
log.info("No WorkItem watchdog_warn timeout defined, using watchdog value");
return watchdogTimeout();
} else {
return Duration.parse(config.get("runner").get("watchdog_warn").asString());
}
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/BotTaskAggregationHandler.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.*;
public abstract class BotTaskAggregationHandler extends StreamHandler {
private static class ThreadLogs {
boolean isPublishing;
boolean inTask;
List logs;
ThreadLogs() {
isPublishing = false;
clear();
}
void clear() {
inTask = false;
logs = new ArrayList<>();
}
}
private final Map threadLogs;
private final Logger log;
// Should this class handle log level filtering or leave that to the subclass
private final boolean filterOnLevel;
public BotTaskAggregationHandler(boolean filterOnLevel) {
this.filterOnLevel = filterOnLevel;
threadLogs = new ConcurrentHashMap<>();
log = Logger.getLogger("org.openjdk.skara.bot");
}
private boolean hasMarker(LogRecord record, BotRunner.TaskPhases marker) {
if (record.getParameters() == null) {
return false;
}
return Arrays.asList(record.getParameters()).contains(marker);
}
@Override
public final void publish(LogRecord record) {
var newEntry = new ThreadLogs();
var threadEntry = threadLogs.putIfAbsent(record.getLongThreadID(), newEntry);
if (threadEntry == null) {
threadEntry = newEntry;
}
// Avoid potential recursive log output
if (threadEntry.isPublishing) {
return;
}
threadEntry.isPublishing = true;
try {
if (!threadEntry.inTask) {
if (!hasMarker(record, BotRunner.TaskPhases.BEGIN)) {
if (!filterOnLevel || record.getLevel().intValue() >= getLevel().intValue()) {
publishSingle(record);
}
return;
}
threadEntry.inTask = true;
}
if (!filterOnLevel || record.getLevel().intValue() >= getLevel().intValue()) {
threadEntry.logs.add(record);
}
if (hasMarker(record, BotRunner.TaskPhases.END)) {
publishAggregated(threadEntry.logs);
threadEntry.clear();
}
}
catch (RuntimeException e) {
log.log(Level.SEVERE, "Exception during task notification posting: " + e.getMessage(), e);
} finally {
threadEntry.isPublishing = false;
}
}
public abstract void publishAggregated(List task);
public abstract void publishSingle(LogRecord record);
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/BotWatchdog.java
================================================
/*
* Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import java.time.Duration;
public class BotWatchdog {
private final Thread watchThread;
private final Duration maxWait;
private final Runnable callBack;
private volatile boolean hasBeenPinged = false;
private void threadMain() {
while (true) {
try {
Thread.sleep(maxWait);
if (!hasBeenPinged) {
System.out.println("No watchdog ping detected for " + maxWait + " - exiting...");
callBack.run();
System.exit(1);
}
hasBeenPinged = false;
} catch (InterruptedException ignored) {
}
}
}
BotWatchdog(Duration maxWait, Runnable callBack) {
this.maxWait = maxWait;
this.callBack = callBack;
watchThread = new Thread(this::threadMain);
watchThread.setName("BotWatchdog");
watchThread.setDaemon(true);
watchThread.start();
}
public void ping() {
hasBeenPinged = true;
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/ConfigurationError.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
public class ConfigurationError extends Exception {
ConfigurationError(String message) {
super(message);
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/LivenessHandler.java
================================================
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import org.openjdk.skara.json.JSONObject;
import com.sun.net.httpserver.*;
import java.io.IOException;
import java.util.logging.Logger;
class LivenessHandler implements HttpHandler {
private static final Logger log = Logger.getLogger("org.openjdk.skara.bot");
private final BotRunner runner;
LivenessHandler(BotRunner runner) {
this.runner = runner;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
if (runner.isHealthy()) {
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().close();
} else {
exchange.sendResponseHeaders(500, 0);
exchange.getResponseBody().close();
}
}
static LivenessHandler create(BotRunner runner, JSONObject configuration) {
return new LivenessHandler(runner);
}
static String name() {
return "liveness";
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/LogContext.java
================================================
package org.openjdk.skara.bot;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
/**
* A LogContext is used to temporarily add extra log metadata in the current thread.
* It should be initiated with a try-with-resources construct. The variable itself
* is never used, we only want the controlled automatic close at the end of the try
* block. Typically name the variable __. Example:
*
* try (var __ = new LogContext("foo", "bar")) {
* // some code that logs stuff
* }
*/
public class LogContext implements AutoCloseable {
private static final Logger log = Logger.getLogger("org.openjdk.skara.bot");
private final Map context = new HashMap<>();
public LogContext(String key, String value) {
this.init(Map.of(key, value));
}
public LogContext(Map ctx) {
this.init(ctx);
}
private void init(Map newContext) {
for (var entry : newContext.entrySet()) {
String currentValue = LogContextMap.get(entry.getKey());
if (currentValue != null) {
if (!currentValue.equals(entry.getValue())) {
log.severe("Tried to override the current LogContext value: " + currentValue
+ " for " + entry.getKey() + " with a different value: " + entry.getValue());
}
} else {
this.context.put(entry.getKey(), entry.getValue());
LogContextMap.put(entry.getKey(), entry.getValue());
}
}
}
public void close() {
this.context.forEach((key, value) -> {
LogContextMap.remove(key);
});
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/LogContextMap.java
================================================
package org.openjdk.skara.bot;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* This class holds a static thread local hashmap to store temporary log
* metadata which our custom StreamHandlers can pick up and include in log
* messages.
*/
public class LogContextMap {
private static final ThreadLocal> threadContextMap = new ThreadLocal<>();
public static void put(String key, String value) {
if (threadContextMap.get() == null) {
threadContextMap.set(new HashMap<>());
}
var map = threadContextMap.get();
map.put(key, value);
}
public static String get(String key) {
if (threadContextMap.get() != null) {
return threadContextMap.get().get(key);
} else {
return null;
}
}
public static String remove(String key) {
if (threadContextMap.get() != null) {
return threadContextMap.get().remove(key);
} else {
return null;
}
}
public static Set> entrySet() {
if (threadContextMap.get() != null) {
return threadContextMap.get().entrySet();
} else {
return Collections.emptySet();
}
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/MetricsHandler.java
================================================
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import com.sun.net.httpserver.*;
import org.openjdk.skara.json.*;
import org.openjdk.skara.metrics.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
import java.time.ZonedDateTime;
class MetricsHandler implements HttpHandler {
private static final Logger log = Logger.getLogger("org.openjdk.skara.bot");
private final Exporter exporter;
private MetricsHandler(Exporter exporter) {
this.exporter = exporter;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
var metrics = CollectorRegistry.defaultRegistry().scrape();
var response = exporter.export(metrics);
exchange.sendResponseHeaders(200, response.length());
var output = exchange.getResponseBody();
output.write(response.getBytes(StandardCharsets.UTF_8));
output.close();
}
static MetricsHandler create(BotRunner runner, JSONObject configuration) {
return new MetricsHandler(new PrometheusExporter());
}
static String name() {
return "metrics";
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/ProfileHandler.java
================================================
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import org.openjdk.skara.json.JSONObject;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.logging.*;
import com.sun.net.httpserver.*;
import jdk.jfr.*;
import java.text.ParseException;
import java.util.concurrent.locks.ReentrantLock;
class ProfileHandler implements HttpHandler {
private static final Logger log = Logger.getLogger("org.openjdk.skara.bot");
private final Path configurationPath;
private final int maxDuration;
private final String token;
private final ReentrantLock lock = new ReentrantLock();
private ProfileHandler(Path configurationPath, int maxDuration, String token) {
this.configurationPath = configurationPath;
this.maxDuration = maxDuration;
this.token = token;
}
private static Map parameters(HttpExchange exchange) {
var query = exchange.getRequestURI().getQuery();
var parts = query.split("&");
var result = new HashMap();
for (var part : parts) {
var keyAndValue = part.split("=");
result.put(keyAndValue[0], keyAndValue[1]);
}
return result;
}
private void handleLocked(HttpExchange exchange) throws IOException {
var params = parameters(exchange);
var seconds = params.getOrDefault("seconds", "30");
var configurationName = params.getOrDefault("configuration", "profile");
Configuration configuration = null;
try {
configuration = Configuration.create(configurationPath);
} catch (ParseException e) {
log.log(Level.WARNING, "Could not get JFR configuration", e);
exchange.sendResponseHeaders(500, 0);
exchange.getResponseBody().close();
}
log.info("Profiling for " + seconds + " seconds with configuration " + configurationName);
var recording = new Recording(configuration);
recording.start();
try {
var duration = Integer.parseInt(seconds);
if (duration > maxDuration) {
duration = maxDuration;
}
Thread.sleep(duration * 1000);
} catch (InterruptedException e) {
log.log(Level.WARNING, "Thread interrupted when sleeping", e);
exchange.sendResponseHeaders(500, 0);
exchange.getResponseBody().close();
}
recording.stop();
var path = Files.createTempFile("recording", "jfr");
recording.dump(path);
var buffer = new byte[4096];
exchange.sendResponseHeaders(200, Files.size(path));
try (var output = exchange.getResponseBody(); var stream = Files.newInputStream(path)) {
while (true) {
var read = stream.read(buffer);
if (read == -1) {
break;
}
output.write(buffer, 0, read);
}
} catch (Throwable t) {
log.log(Level.WARNING, "Could not send JFR recording", t);
} finally {
Files.deleteIfExists(path);
}
}
@Override
public void handle(HttpExchange exchange) throws IOException {
var authHeader = exchange.getRequestHeaders().getFirst("Authorization");
if (authHeader == null) {
log.log(Level.WARNING, "Authorization HTTP header missing");
exchange.sendResponseHeaders(401, 0);
exchange.getResponseBody().close();
return;
}
var authParts = authHeader.split(" ");
if (authParts.length != 2 || !authParts[0].equals("token")) {
log.log(Level.WARNING, "Authorization HTTP header has wrong format");
exchange.sendResponseHeaders(401, 0);
exchange.getResponseBody().close();
return;
}
if (!authParts[1].equals(token)) {
log.log(Level.WARNING, "Wrong authorization token: " + authParts[1]);
exchange.sendResponseHeaders(401, 0);
exchange.getResponseBody().close();
return;
}
// Only allow one recording at a time.
lock.lock();
try {
handleLocked(exchange);
} finally {
lock.unlock();
}
}
static ProfileHandler create(BotRunner runner, JSONObject configuration) {
var configurationPath = Path.of(configuration.get("configuration").asString());
var maxDuration = configuration.get("max-duration").asInt();
var token = configuration.get("token").asString();
return new ProfileHandler(configurationPath, maxDuration, token);
}
static String name() {
return "profile";
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/ReadinessHandler.java
================================================
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import org.openjdk.skara.json.JSONObject;
import com.sun.net.httpserver.*;
import java.io.IOException;
import java.util.logging.Logger;
class ReadinessHandler implements HttpHandler {
private static final Logger log = Logger.getLogger("org.openjdk.skara.bot");
private final BotRunner runner;
ReadinessHandler(BotRunner runner) {
this.runner = runner;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
if (runner.isReady()) {
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().close();
} else {
exchange.sendResponseHeaders(404, 0);
exchange.getResponseBody().close();
}
}
static ReadinessHandler create(BotRunner runner, JSONObject configuration) {
return new ReadinessHandler(runner);
}
static String name() {
return "readiness";
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/VersionHandler.java
================================================
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import org.openjdk.skara.json.JSONObject;
import org.openjdk.skara.version.Version;
import com.sun.net.httpserver.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.logging.Logger;
class VersionHandler implements HttpHandler {
private static final Logger log = Logger.getLogger("org.openjdk.skara.bot");
@Override
public void handle(HttpExchange exchange) throws IOException {
var version = Version.fromManifest();
if (version.isPresent()) {
var bytes = version.get().getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, bytes.length);
exchange.getResponseBody().write(bytes);
exchange.getResponseBody().close();
} else {
exchange.sendResponseHeaders(500, 0);
exchange.getResponseBody().close();
}
}
static VersionHandler create(BotRunner runner, JSONObject configuration) {
return new VersionHandler();
}
static String name() {
return "version";
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/WebhookHandler.java
================================================
/*
* Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import com.sun.net.httpserver.*;
import org.openjdk.skara.json.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
class WebhookHandler implements HttpHandler {
private final static Logger log = Logger.getLogger("org.openjdk.skara.bot");
private final BotRunner runner;
private WebhookHandler(BotRunner runner) {
this.runner = runner;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
var input = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
JSONValue json = null;
try {
json = JSON.parse(input);
} catch (Exception e) {
log.log(Level.WARNING, "Failed to parse incoming request: " + input, e);
exchange.sendResponseHeaders(400, 0);
exchange.getResponseBody().close();
return;
}
// Reply immediately
var response = "{}";
exchange.sendResponseHeaders(200, response.length());
var output = exchange.getResponseBody();
output.write(response.getBytes(StandardCharsets.UTF_8));
output.close();
runner.processWebhook(json);
}
static String name() {
return "webhook";
}
static WebhookHandler create(BotRunner runner, JSONObject configuration) {
return new WebhookHandler(runner);
}
}
================================================
FILE: bot/src/main/java/org/openjdk/skara/bot/WorkItem.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import java.nio.file.Path;
import java.util.*;
public interface WorkItem {
/**
* Return true if this item can run concurrently with other, otherwise false.
* @param other
* @return
*/
boolean concurrentWith(WorkItem other);
/**
* Returns true if this item should replace the other item in the queue. By default
* this is true if both items are of the same type, and cannot run concurrently with
* each other. In some cases we need a more specific condition.
*/
default boolean replaces(WorkItem other) {
return this.getClass().equals(other.getClass()) && !concurrentWith(other);
}
/**
* Execute the appropriate tasks with the provided scratch folder. Optionally return follow-up work items
* that will be scheduled for execution.
* @param scratchPath
* @return A collection of follow-up work items, allowed to be empty (or null) if none are needed.
*/
Collection run(Path scratchPath);
String botName();
String workItemName();
/**
* The BotRunner will catch RuntimeExceptions, implementing this method allows a WorkItem to
* perform additional cleanup if necessary (avoiding the need for catching and rethrowing the exception).
* @param e
*/
default void handleRuntimeException(RuntimeException e) {}
}
================================================
FILE: bot/src/test/java/org/openjdk/skara/bot/BotRunnerConfigurationTests.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import org.openjdk.skara.json.JSON;
import org.junit.jupiter.api.Test;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.*;
class BotRunnerConfigurationTests {
@Test
void storageFolder() throws ConfigurationError {
var input = JSON.object().put("storage", JSON.object().put("path", "/x"))
.put("xbot", JSON.object());
var cfg = BotRunnerConfiguration.parse(input);
var botCfg = cfg.perBotConfiguration("xbot");
assertEquals(Path.of("/x/xbot"), botCfg.storageFolder());
}
@Test
void parseHost() throws ConfigurationError {
var input = JSON.object()
.put("xbot",
JSON.object().put("repository", "test/x/y"));
var cfg = BotRunnerConfiguration.parse(input);
var botCfg = cfg.perBotConfiguration("xbot");
var error = assertThrows(RuntimeException.class, () -> botCfg.repository("test/x/y"));
assertEquals("Repository entry test/x/y uses undefined host 'test'", error.getCause().getMessage());
}
@Test
void parseRef() throws ConfigurationError {
var input = JSON.object()
.put("xbot",
JSON.object().put("repository", "test/x/y:z"));
var cfg = BotRunnerConfiguration.parse(input);
var botCfg = cfg.perBotConfiguration("xbot");
var error = assertThrows(RuntimeException.class, () -> botCfg.repositoryRef("test/x/y:z"));
assertEquals("Repository entry test/x/y uses undefined host 'test'", error.getCause().getMessage());
}
@Test
void parseName() throws ConfigurationError {
var empty = JSON.object().put("xbot", JSON.object());
var cfg = BotRunnerConfiguration.parse(empty);
assertEquals("repo", cfg.perBotConfiguration("xbot").repositoryName("repo"));
assertEquals("repo", cfg.perBotConfiguration("xbot").repositoryName("host/org/repo"));
assertEquals("repo", cfg.perBotConfiguration("xbot").repositoryName("host/org/repo:ref"));
assertEquals("repo", cfg.perBotConfiguration("xbot").repositoryName("host/org/repo:nested/ref"));
assertEquals("repo", cfg.perBotConfiguration("xbot").repositoryName("user@host/org/repo:nested/ref"));
}
}
================================================
FILE: bot/src/test/java/org/openjdk/skara/bot/BotRunnerTests.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.condition.EnabledOnOs;
import static org.junit.jupiter.api.condition.OS.LINUX;
import static org.junit.jupiter.api.condition.OS.MAC;
import org.openjdk.skara.json.JSON;
import java.nio.file.Path;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Supplier;
import java.util.logging.*;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TestWorkItem implements WorkItem {
private final ConcurrencyCheck concurrencyCheck;
private final String description;
boolean hasRun = false;
interface ConcurrencyCheck {
boolean concurrentWith(WorkItem other);
}
TestWorkItem(ConcurrencyCheck concurrencyCheck) {
this.concurrencyCheck = concurrencyCheck;
this.description = null;
}
TestWorkItem(ConcurrencyCheck concurrencyCheck, String description) {
this.concurrencyCheck = concurrencyCheck;
this.description = description;
}
@Override
public Collection run(Path scratchPath) {
hasRun = true;
System.out.println("Item " + this.toString() + " now running");
return List.of();
}
@Override
public boolean concurrentWith(WorkItem other) {
return concurrencyCheck.concurrentWith(other);
}
@Override
public String toString() {
return description != null ? description : super.toString();
}
@Override
public String botName() {
return "test-bot";
}
@Override
public String workItemName() {
return botName();
}
}
class TestWorkItemChild extends TestWorkItem {
TestWorkItemChild(ConcurrencyCheck concurrencyCheck, String description) {
super(concurrencyCheck, description);
}
}
class TestWorkItemWithFollowup extends TestWorkItem {
private List followUpItems;
TestWorkItemWithFollowup(ConcurrencyCheck concurrencyCheck, String description, List followUpItems) {
super(concurrencyCheck, description);
this.followUpItems = followUpItems;
}
@Override
public Collection run(Path scratchPath) {
hasRun = true;
System.out.println("Item with followups " + this.toString() + " now running");
return followUpItems;
}
}
class TestBlockedWorkItem implements WorkItem {
private final CountDownLatch countDownLatch;
TestBlockedWorkItem(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public boolean concurrentWith(WorkItem other) {
return false;
}
@Override
public Collection run(Path scratchPath) {
System.out.println("Starting to wait...");;
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Done waiting");
return List.of();
}
@Override
public String botName() {
return "test-blocked";
}
@Override
public String workItemName() {
return botName();
}
}
class TestBot implements Bot {
private final List items;
private final Supplier> itemSupplier;
TestBot(WorkItem... items) {
this.items = Arrays.asList(items);
itemSupplier = null;
}
TestBot(Supplier> itemSupplier) {
items = null;
this.itemSupplier = itemSupplier;
}
@Override
public List getPeriodicItems() {
if (items != null) {
return items;
} else {
return itemSupplier.get();
}
}
@Override
public String name() {
return "test-bot";
}
}
class BotRunnerTests {
@BeforeAll
static void setUp() {
Logger log = Logger.getGlobal();
log.setLevel(Level.FINER);
log = Logger.getLogger("org.openjdk.bots.cli");
log.setLevel(Level.FINER);
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.FINER);
log.addHandler(handler);
}
private BotRunnerConfiguration config() {
var config = JSON.object();
try {
return BotRunnerConfiguration.parse(config);
} catch (ConfigurationError configurationError) {
throw new RuntimeException(configurationError);
}
}
private BotRunnerConfiguration config(String json) {
var config = JSON.parse(json).asObject();
try {
return BotRunnerConfiguration.parse(config);
} catch (ConfigurationError configurationError) {
throw new RuntimeException(configurationError);
}
}
@Test
void simpleConcurrent() throws TimeoutException {
var item1 = new TestWorkItem(i -> true, "Item 1");
var item2 = new TestWorkItem(i -> true, "Item 2");
var bot = new TestBot(item1, item2);
var runner = new BotRunner(config(), List.of(bot));
runner.runOnce(Duration.ofSeconds(10));
assertTrue(item1.hasRun);
assertTrue(item2.hasRun);
}
@Test
void simpleSerial() throws TimeoutException {
var item1 = new TestWorkItem(i -> false, "Item 1");
var item2 = new TestWorkItem(i -> false, "Item 2");
var bot = new TestBot(item1, item2);
var runner = new BotRunner(config(), List.of(bot));
runner.runOnce(Duration.ofSeconds(10));
assertTrue(item1.hasRun);
assertTrue(item2.hasRun);
}
@Test
void moreItemsThanScratchPaths() throws TimeoutException {
List items = new LinkedList<>();
for (int i = 0; i < 20; ++i) {
items.add(new TestWorkItem(x -> true, "Item " + i));
}
var bot = new TestBot(items.toArray(new TestWorkItem[0]));
var runner = new BotRunner(config(), List.of(bot));
runner.runOnce(Duration.ofSeconds(10));
for (var item : items) {
assertTrue(item.hasRun);
}
}
static class ThrowingItemProvider {
private final List items;
private int throwCount;
ThrowingItemProvider(List items, int throwCount) {
this.items = items;
this.throwCount = throwCount;
}
List get() {
if (throwCount-- > 0) {
throw new RuntimeException("Sorry, can't provide items just yet");
} else {
return items;
}
}
}
@Test
void periodItemsThrow() throws TimeoutException {
var item1 = new TestWorkItem(i -> false, "Item 1");
var item2 = new TestWorkItem(i -> false, "Item 2");
var provider = new ThrowingItemProvider(List.of(item1, item2), 1);
var bot = new TestBot(provider::get);
new BotRunner(config(), List.of(bot)).runOnce(Duration.ofSeconds(10));
Assertions.assertFalse(item1.hasRun);
Assertions.assertFalse(item2.hasRun);
new BotRunner(config(), List.of(bot)).runOnce(Duration.ofSeconds(10));
assertTrue(item1.hasRun);
assertTrue(item2.hasRun);
}
@Test
void discardAdditionalBlockedItems() throws TimeoutException {
var item1 = new TestWorkItem(i -> false, "Item 1");
var item2 = new TestWorkItem(i -> false, "Item 2");
var item3 = new TestWorkItem(i -> false, "Item 3");
var item4 = new TestWorkItem(i -> false, "Item 4");
var bot = new TestBot(item1, item2, item3, item4);
var config = config("{\"runner\": { \"concurrency\": 1 } }");
var runner = new BotRunner(config, List.of(bot));
runner.runOnce(Duration.ofSeconds(10));
assertTrue(item1.hasRun);
Assertions.assertFalse(item2.hasRun);
Assertions.assertFalse(item3.hasRun);
assertTrue(item4.hasRun);
}
@Test
void dontDiscardDifferentBlockedItems() throws TimeoutException {
var item1 = new TestWorkItem(i -> false, "Item 1");
var item2 = new TestWorkItem(i -> false, "Item 2");
var item3 = new TestWorkItem(i -> false, "Item 3");
var item4 = new TestWorkItem(i -> false, "Item 4");
var item5 = new TestWorkItemChild(i -> false, "Item 5");
var item6 = new TestWorkItemChild(i -> false, "Item 6");
var item7 = new TestWorkItemChild(i -> false, "Item 7");
var bot = new TestBot(item1, item2, item3, item4, item5, item6, item7);
var config = config("{\"runner\": { \"concurrency\": 1 } }");
var runner = new BotRunner(config, List.of(bot));
runner.runOnce(Duration.ofSeconds(10));
assertTrue(item1.hasRun);
Assertions.assertFalse(item2.hasRun);
Assertions.assertFalse(item3.hasRun);
assertTrue(item4.hasRun);
Assertions.assertFalse(item5.hasRun);
Assertions.assertFalse(item6.hasRun);
assertTrue(item7.hasRun);
}
@Test
@EnabledOnOs({LINUX, MAC})
void watchdogTrigger() throws TimeoutException {
var countdownLatch = new CountDownLatch(1);
var item = new TestBlockedWorkItem(countdownLatch);
var bot = new TestBot(item);
var runner = new BotRunner(config("{ \"runner\": { \"watchdog_warn\": \"PT0.01S\", \"interval\": \"PT0.001S\" } }"), List.of(bot));
var errors = new ArrayList();
var log = Logger.getLogger("org.openjdk.skara.bot");
log.addHandler(new Handler() {
@Override
public void publish(LogRecord record) {
if (record.getLevel().equals(Level.SEVERE)) {
errors.add(record.getMessage());
}
}
@Override
public void flush() {
}
@Override
public void close() throws SecurityException {
}
});
runner.run(Duration.ofMillis(100));
assertTrue(errors.size() > 0);
assertTrue(errors.size() <= 100);
countdownLatch.countDown();
}
@Test
void dependentItems() throws TimeoutException {
var item2 = new TestWorkItem(i -> true, "Item 2");
var item3 = new TestWorkItem(i -> true, "Item 3");
var item1 = new TestWorkItemWithFollowup(i -> true, "Item 1", List.of(item2, item3));
var bot = new TestBot(item1);
var runner = new BotRunner(config(), List.of(bot));
runner.runOnce(Duration.ofSeconds(10));
assertTrue(item1.hasRun);
assertTrue(item2.hasRun);
assertTrue(item3.hasRun);
}
}
================================================
FILE: bot/src/test/java/org/openjdk/skara/bot/BotTaskAggregationHandlerTests.java
================================================
/*
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bot;
import org.junit.jupiter.api.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.logging.*;
import java.util.stream.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
class TestBotTaskAggregationHandler extends BotTaskAggregationHandler {
private final Collection nonTaskRecords;
private final Collection> taskRecords;
TestBotTaskAggregationHandler() {
super(false);
nonTaskRecords = new ConcurrentLinkedQueue<>();
taskRecords = new ConcurrentLinkedQueue<>();
}
@Override
public void publishAggregated(List task) {
taskRecords.add(task);
}
@Override
public void publishSingle(LogRecord record) {
nonTaskRecords.add(record);
}
Collection> taskRecords() {
return taskRecords;
}
Collection nonTaskRecords() {
return nonTaskRecords;
}
}
class BotTaskAggregationHandlerTests {
@BeforeAll
static void setUp() {
Logger log = Logger.getGlobal();
log.setLevel(Level.FINEST);
ConsoleHandler handler = new ConsoleHandler();
handler.setLevel(Level.FINER);
log.addHandler(handler);
}
@Test
void simpleNonTask() {
Logger log = Logger.getGlobal();
var handler = new TestBotTaskAggregationHandler();
handler.setLevel(Level.FINER);
log.addHandler(handler);
log.fine("Not a task log");
assertEquals(0, handler.taskRecords().size());
assertEquals(1, handler.nonTaskRecords().size());
}
@Test
void simpleTask() {
Logger log = Logger.getGlobal();
var handler = new TestBotTaskAggregationHandler();
handler.setLevel(Level.FINER);
log.addHandler(handler);
log.log(Level.FINE, "Task log start", BotRunner.TaskPhases.BEGIN);
log.log(Level.FINE, "Task log end", BotRunner.TaskPhases.END);
assertEquals(1, handler.taskRecords().size());
assertEquals(0, handler.nonTaskRecords().size());
}
static class ConcurrentTask implements Runnable {
private final CountDownLatch countDownLatch;
private final int numLoops;
ConcurrentTask(CountDownLatch countDownLatch, int numLoops) {
this.countDownLatch = countDownLatch;
this.numLoops = numLoops;
}
@Override
public void run() {
try {
Logger log = Logger.getGlobal();
countDownLatch.await();
for (int i = 0; i < numLoops; ++i) {
log.log(Level.FINEST, Long.toString(Thread.currentThread().threadId()), BotRunner.TaskPhases.BEGIN);
log.log(Level.FINEST, Long.toString(Thread.currentThread().threadId()), BotRunner.TaskPhases.END);
log.log(Level.FINEST, Long.toString(Thread.currentThread().threadId()));
}
} catch (InterruptedException e) {
fail(e);
}
}
}
@Test
void concurrentSeparation() {
final int concurrency = 50;
final int numLoops = 100;
Logger log = Logger.getGlobal();
var handler = new TestBotTaskAggregationHandler();
handler.setLevel(Level.FINEST);
log.addHandler(handler);
var countDownLatch = new CountDownLatch(1);
var threads = IntStream.range(0, concurrency)
.mapToObj(num -> new Thread(new ConcurrentTask(countDownLatch, numLoops)))
.collect(Collectors.toList());
threads.forEach(Thread::start);
countDownLatch.countDown();
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
fail(e);
}
});
assertEquals(concurrency * numLoops, handler.taskRecords().size());
assertEquals(concurrency * numLoops, handler.nonTaskRecords().size());
handler.taskRecords().stream()
.flatMap(Collection::stream)
.forEach(record -> assertEquals(Long.toString(record.getLongThreadID()), record.getMessage()));
handler.nonTaskRecords()
.forEach(record -> assertEquals(Long.toString(record.getLongThreadID()), record.getMessage()));
}
}
================================================
FILE: bot/src/test/java/org/openjdk/skara/bot/LogContextTests.java
================================================
package org.openjdk.skara.bot;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
public class LogContextTests {
@Test
public void simple() {
String key = "keyname";
assertNull(LogContextMap.get(key), "Key " + key + " already present in context");
try (var __ = new LogContext(key, "value")) {
assertEquals("value", LogContextMap.get(key), "Context property not set");
}
assertNull(LogContextMap.get(key), "Context property not removed");
}
}
================================================
FILE: bots/bridgekeeper/build.gradle
================================================
/*
* Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module {
name = 'org.openjdk.skara.bots.bridgekeeper'
test {
requires 'org.junit.jupiter.api'
requires 'org.openjdk.skara.vcs'
requires 'org.openjdk.skara.test'
opens 'org.openjdk.skara.bots.bridgekeeper' to 'org.junit.platform.commons'
}
}
dependencies {
implementation project(':ci')
implementation project(':host')
implementation project(':forge')
implementation project(':issuetracker')
implementation project(':bot')
implementation project(':census')
implementation project(':json')
implementation project(':vcs')
implementation project(':metrics')
testImplementation project(':test')
}
================================================
FILE: bots/bridgekeeper/src/main/java/module-info.java
================================================
import org.openjdk.skara.bots.bridgekeeper.BridgekeeperBotFactory;
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module org.openjdk.skara.bots.bridgekeeper {
requires org.openjdk.skara.bot;
requires java.logging;
provides org.openjdk.skara.bot.BotFactory with BridgekeeperBotFactory;
}
================================================
FILE: bots/bridgekeeper/src/main/java/org/openjdk/skara/bots/bridgekeeper/BridgekeeperBotFactory.java
================================================
/*
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.bridgekeeper;
import org.openjdk.skara.bot.*;
import org.openjdk.skara.forge.HostedRepository;
import org.openjdk.skara.json.JSONValue;
import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;
public class BridgekeeperBotFactory implements BotFactory {
static final String NAME = "bridgekeeper";
@Override
public String name() {
return NAME;
}
@Override
public List create(BotConfiguration configuration) {
var ret = new ArrayList();
var specific = configuration.specific();
for (var repo : specific.get("mirrors").asArray()) {
var bot = new PullRequestCloserBot(configuration.repository(repo.asString()), PullRequestCloserBot.Type.MIRROR);
ret.add(bot);
}
for (var repo : specific.get("data").asArray()) {
var bot = new PullRequestCloserBot(configuration.repository(repo.asString()), PullRequestCloserBot.Type.DATA);
ret.add(bot);
}
var pruned = new HashMap();
var ignoredUsers = specific.get("pruned").get("ignored").get("users").stream()
.map(JSONValue::asString)
.collect(Collectors.toSet());
for (var repo : specific.get("pruned").get("repositories").fields()) {
var maxAge = Duration.parse(repo.value().get("maxage").asString());
pruned.put(configuration.repository(repo.name()), maxAge);
}
if (!pruned.isEmpty()) {
var bot = new PullRequestPrunerBot(pruned, ignoredUsers);
ret.add(bot);
}
return ret;
}
}
================================================
FILE: bots/bridgekeeper/src/main/java/org/openjdk/skara/bots/bridgekeeper/PullRequestCloserBot.java
================================================
/*
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.bridgekeeper;
import org.openjdk.skara.bot.*;
import org.openjdk.skara.forge.*;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.logging.Logger;
class PullRequestCloserBotWorkItem implements WorkItem {
private final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
private final HostedRepository repository;
private final PullRequest pr;
private final Consumer errorHandler;
private final PullRequestCloserBot.Type type;
PullRequestCloserBotWorkItem(HostedRepository repository, PullRequest pr, PullRequestCloserBot.Type type, Consumer errorHandler) {
this.pr = pr;
this.repository = repository;
this.type = type;
this.errorHandler = errorHandler;
}
private static final String WELCOME_MARKER = "";
private void checkWelcomeMessage() {
log.info("Checking welcome message of " + pr);
var comments = pr.comments();
var welcomePosted = comments.stream()
.anyMatch(comment -> comment.body().contains(WELCOME_MARKER));
if (!welcomePosted) {
String message = null;
if (type == PullRequestCloserBot.Type.MIRROR) {
message = "Welcome to the OpenJDK organization on GitHub!\n\n" +
"This repository is currently a read-only git mirror of the official Mercurial " +
"repository (located at https://hg.openjdk.org/). As such, we are not " +
"currently accepting pull requests here. If you would like to contribute to " +
"the OpenJDK project, please see https://openjdk.org/contribute/ on how " +
"to proceed.\n\n" +
"This pull request will be automatically closed.";
} else if (type == PullRequestCloserBot.Type.DATA) {
message = "Welcome to the OpenJDK organization on GitHub!\n\n" +
"This repository currently holds only automatically generated data and therefore does not accept pull requests." +
"This pull request will be automatically closed.";
} else {
message = "Welcome to the OpenJDK organization on GitHub!\n\n" +
"This repository does not currently accept pull requests." +
"This pull request will be automatically closed.";
}
log.fine("Posting welcome message");
pr.addComment(WELCOME_MARKER + "\n\n" + message);
}
pr.setState(PullRequest.State.CLOSED);
}
@Override
public boolean concurrentWith(WorkItem other) {
if (!(other instanceof PullRequestCloserBotWorkItem otherItem)) {
return true;
}
if (!pr.isSame(otherItem.pr)) {
return true;
}
return false;
}
@Override
public Collection run(Path scratchPath) {
checkWelcomeMessage();
return List.of();
}
@Override
public void handleRuntimeException(RuntimeException e) {
errorHandler.accept(e);
}
@Override
public String toString() {
return "PullRequestCloserBotWorkItem@" + repository.name() + "#" + pr.id();
}
@Override
public String botName() {
return BridgekeeperBotFactory.NAME;
}
@Override
public String workItemName() {
return "closer";
}
}
public class PullRequestCloserBot implements Bot {
private final HostedRepository remoteRepo;
private final PullRequestUpdateCache updateCache;
public enum Type {
MIRROR,
DATA
}
private final Type type;
PullRequestCloserBot(HostedRepository repo, Type type) {
this.remoteRepo = repo;
this.updateCache = new PullRequestUpdateCache();
this.type = type;
}
@Override
public List getPeriodicItems() {
List ret = new LinkedList<>();
for (var pr : remoteRepo.openPullRequests()) {
if (updateCache.needsUpdate(pr)) {
var item = new PullRequestCloserBotWorkItem(remoteRepo, pr, type, e -> updateCache.invalidate(pr));
ret.add(item);
}
}
return ret;
}
@Override
public String name() {
return BridgekeeperBotFactory.NAME;
}
@Override
public String toString() {
return "PullRequestCloserBot@" + remoteRepo.name();
}
public Type getType() {
return type;
}
}
================================================
FILE: bots/bridgekeeper/src/main/java/org/openjdk/skara/bots/bridgekeeper/PullRequestPrunerBot.java
================================================
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.bridgekeeper;
import org.openjdk.skara.bot.*;
import org.openjdk.skara.forge.*;
import org.openjdk.skara.issuetracker.Comment;
import java.nio.file.Path;
import java.time.*;
import java.util.*;
import java.util.function.Function;
import java.util.logging.Logger;
import java.util.stream.*;
class PullRequestPrunerBotWorkItem implements WorkItem {
private final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
private final PullRequest pr;
private final Duration maxAge;
private final Set ignoredUsers;
PullRequestPrunerBotWorkItem(PullRequest pr, Duration maxAge, Set ignoredUsers) {
this.pr = pr;
this.maxAge = maxAge;
this.ignoredUsers = ignoredUsers;
}
@Override
public boolean concurrentWith(WorkItem other) {
if (!(other instanceof PullRequestPrunerBotWorkItem otherItem)) {
return true;
}
if (!pr.isSame(otherItem.pr)) {
return true;
}
return false;
}
// Prune durations are on the order of days and weeks
private String formatDuration(Duration duration) {
var count = duration.toDays();
var unit = "day";
if (count > 14) {
count /= 7;
unit = "week";
}
if (count != 1) {
unit += "s";
}
return count + " " + unit;
}
private static final String NOTICE_MARKER = "";
@Override
public Collection run(Path scratchPath) {
var comments = pr.comments();
if (comments.size() > 0) {
var lastComment = comments.stream()
.filter(comment -> !ignoredUsers.contains(comment.author().username()))
.toList()
.getLast();
if (lastComment.author().equals(pr.repository().forge().currentUser()) && lastComment.body().contains(NOTICE_MARKER)
&& !lastComment.createdAt().isBefore(pr.lastTouchedTime())) {
var message = "@" + pr.author().username() + " This pull request has been inactive for more than " +
formatDuration(maxAge.multipliedBy(2)) + " and will now be automatically closed. If you would " +
"like to continue working on this pull request in the future, feel free to reopen it! This can be done " +
"using the `/open` pull request command.";
log.fine("Posting prune message");
pr.addComment(message);
pr.setState(PullRequest.State.CLOSED);
return List.of();
}
}
var message = "@" + pr.author().username() + " This pull request has been inactive for more than " +
formatDuration(maxAge) + " and will be automatically closed if another " + formatDuration(maxAge) +
" passes without any activity. To avoid this, simply issue a `/touch` or `/keepalive` command to the pull request. Feel free " +
"to ask for assistance if you need help with progressing this pull request towards integration!";
log.fine("Posting prune notification message");
pr.addComment(NOTICE_MARKER + "\n\n" + message);
return List.of();
}
@Override
public String toString() {
return "PullRequestPrunerBotWorkItem@" + pr.repository().name() + "#" + pr.id();
}
@Override
public String botName() {
return BridgekeeperBotFactory.NAME;
}
@Override
public String workItemName() {
return "pruner";
}
}
public class PullRequestPrunerBot implements Bot {
private final Map maxAges;
private final Deque repositoriesToCheck = new LinkedList<>();
private final Deque pullRequestToCheck = new LinkedList<>();
private final Logger log = Logger.getLogger("org.openjdk.skara.bots.bridgekeeper");
private Duration currentMaxAge;
private Set ignoredUsers;
PullRequestPrunerBot(Map maxAges, Set ignoredUsers) {
this.maxAges = maxAges;
this.ignoredUsers = ignoredUsers;
}
@Override
public List getPeriodicItems() {
List ret = new LinkedList<>();
if (repositoriesToCheck.isEmpty()) {
repositoriesToCheck.addAll(maxAges.keySet());
}
if (pullRequestToCheck.isEmpty()) {
var nextRepository = repositoriesToCheck.pollFirst();
if (nextRepository == null) {
log.warning("No repositories configured for pruning");
return ret;
}
currentMaxAge = maxAges.get(nextRepository);
pullRequestToCheck.addAll(nextRepository.openPullRequests());
}
var pr = pullRequestToCheck.pollFirst();
if (pr == null) {
log.info("No prune candidates found - skipping");
return ret;
}
// Latest prune-delaying action (deliberately excluding pr.updatedAt, as it can be updated spuriously)
var latestAction = Stream.of(Stream.of(pr.createdAt(), pr.lastTouchedTime()),
pr.comments().stream()
.filter(comment -> !ignoredUsers.contains(comment.author().username()))
.map(Comment::updatedAt),
pr.reviews().stream()
.map(Review::createdAt),
pr.reviewCommentsAsComments().stream()
.map(Comment::updatedAt))
.flatMap(Function.identity())
.max(ZonedDateTime::compareTo).orElseThrow();
var actualMaxAge = pr.isDraft() ? currentMaxAge.multipliedBy(2) : currentMaxAge;
var oldestAllowed = ZonedDateTime.now().minus(actualMaxAge);
if (latestAction.isBefore(oldestAllowed)) {
var item = new PullRequestPrunerBotWorkItem(pr, actualMaxAge, ignoredUsers);
ret.add(item);
}
return ret;
}
@Override
public String name() {
return BridgekeeperBotFactory.NAME;
}
@Override
public String toString() {
return "PullRequestPrunerBot";
}
public Map getMaxAges() {
return maxAges;
}
public Set getIgnoredUsers() {
return ignoredUsers;
}
}
================================================
FILE: bots/bridgekeeper/src/test/java/org/openjdk/skara/bots/bridgekeeper/BridgekeeperBotFactoryTest.java
================================================
/*
* Copyright (c) 2022, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.bridgekeeper;
import org.junit.jupiter.api.Test;
import org.openjdk.skara.json.JWCC;
import org.openjdk.skara.test.TestBotFactory;
import org.openjdk.skara.test.TestHostedRepository;
import java.time.Duration;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class BridgekeeperBotFactoryTest {
@Test
public void testCreate() {
String jsonString = """
{
"mirrors": [
"mirror1",
"mirror2",
"mirror3"
],
"data": [
"data1",
"data2",
"data3"
],
"pruned": {
"ignored": {
"users": [
"user1",
"user2"
]
},
"repositories": {
"pruned1": {
"maxage": "P1D"
},
"pruned2": {
"maxage": "PT48H"
},
"pruned3": {
"maxage": "PT4320M"
}
}
}
}
""";
var jsonConfig = JWCC.parse(jsonString).asObject();
var pruned1 = new TestHostedRepository("pruned1");
var pruned2 = new TestHostedRepository("pruned2");
var pruned3 = new TestHostedRepository("pruned3");
var testBotFactory = TestBotFactory.newBuilder()
.addHostedRepository("mirror1", new TestHostedRepository("mirror1"))
.addHostedRepository("mirror2", new TestHostedRepository("mirror2"))
.addHostedRepository("mirror3", new TestHostedRepository("mirror3"))
.addHostedRepository("data1", new TestHostedRepository("data1"))
.addHostedRepository("data2", new TestHostedRepository("data2"))
.addHostedRepository("data3", new TestHostedRepository("data3"))
.addHostedRepository("pruned1", pruned1)
.addHostedRepository("pruned2", pruned2)
.addHostedRepository("pruned3", pruned3)
.build();
var bots = testBotFactory.createBots(BridgekeeperBotFactory.NAME, jsonConfig);
assertEquals(7, bots.size());
var mirrorPullRequestCloserBots = bots.stream()
.filter(e -> e.getClass().equals(PullRequestCloserBot.class))
.filter(e -> ((PullRequestCloserBot) e).getType().equals(PullRequestCloserBot.Type.MIRROR))
.toList();
var dataPullRequestCloserBots = bots.stream()
.filter(e -> e.getClass().equals(PullRequestCloserBot.class))
.filter(e -> ((PullRequestCloserBot) e).getType().equals(PullRequestCloserBot.Type.DATA))
.toList();
var pullRequestPrunerBots = bots.stream()
.filter(e -> e.getClass().equals(PullRequestPrunerBot.class))
.toList();
// A mirror pullRequestCloserBot for every configured mirror repository
assertEquals(3, mirrorPullRequestCloserBots.size());
// A data pullRequestCloserBot for every configured data repository
assertEquals(3, dataPullRequestCloserBots.size());
// One pullRequestPrunerBot for all configured pruned repository
assertEquals(1, pullRequestPrunerBots.size());
// Check whether each bot is combined with the correct repo
assertEquals("PullRequestCloserBot@mirror1", mirrorPullRequestCloserBots.get(0).toString());
assertEquals("PullRequestCloserBot@mirror2", mirrorPullRequestCloserBots.get(1).toString());
assertEquals("PullRequestCloserBot@mirror3", mirrorPullRequestCloserBots.get(2).toString());
assertEquals("PullRequestCloserBot@data1", dataPullRequestCloserBots.get(0).toString());
assertEquals("PullRequestCloserBot@data2", dataPullRequestCloserBots.get(1).toString());
assertEquals("PullRequestCloserBot@data3", dataPullRequestCloserBots.get(2).toString());
var pullRequestPrunerBot = (PullRequestPrunerBot) pullRequestPrunerBots.get(0);
assertEquals("PullRequestPrunerBot", pullRequestPrunerBot.toString());
var maxAges = pullRequestPrunerBot.getMaxAges();
assertEquals(Duration.ofDays(1), maxAges.get(pruned1));
assertEquals(Duration.ofDays(2), maxAges.get(pruned2));
assertEquals(Duration.ofDays(3), maxAges.get(pruned3));
assertEquals(Set.of("user1", "user2"), pullRequestPrunerBot.getIgnoredUsers());
}
}
================================================
FILE: bots/bridgekeeper/src/test/java/org/openjdk/skara/bots/bridgekeeper/PullRequestCloserBotTests.java
================================================
/*
* Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.bridgekeeper;
import org.openjdk.skara.issuetracker.Issue;
import org.openjdk.skara.test.*;
import org.junit.jupiter.api.*;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.*;
import static org.openjdk.skara.issuetracker.Issue.State.*;
class PullRequestCloserBotTests {
@Test
void simple(TestInfo testInfo) throws IOException {
try (var credentials = new HostCredentials(testInfo);
var tempFolder = new TemporaryDirectory()) {
var author = credentials.getHostedRepository();
var bot = new PullRequestCloserBot(author, PullRequestCloserBot.Type.MIRROR);
// Populate the projects repository
var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
var masterHash = localRepo.resolve("master").orElseThrow();
localRepo.push(masterHash, author.authenticatedUrl(), "master", true);
// Make a change with a corresponding PR
var editHash = CheckableRepository.appendAndCommit(localRepo);
localRepo.push(editHash, author.authenticatedUrl(), "edit", true);
var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
assertEquals(OPEN, pr.state());
// Let the bot see it
TestBotRunner.runPeriodicItems(bot);
// There should now be no open PRs
var prs = author.openPullRequests();
assertEquals(0, prs.size());
var updatedPr = author.pullRequest(pr.id());
assertEquals(CLOSED, updatedPr.state());
}
}
@Test
void keepClosing(TestInfo testInfo) throws IOException {
try (var credentials = new HostCredentials(testInfo);
var tempFolder = new TemporaryDirectory()) {
var author = credentials.getHostedRepository();
var bot = new PullRequestCloserBot(author, PullRequestCloserBot.Type.MIRROR);
// Populate the projects repository
var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
var masterHash = localRepo.resolve("master").orElseThrow();
localRepo.push(masterHash, author.authenticatedUrl(), "master", true);
// Make a change with a corresponding PR
var editHash = CheckableRepository.appendAndCommit(localRepo);
localRepo.push(editHash, author.authenticatedUrl(), "edit", true);
var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
// Let the bot see it
TestBotRunner.runPeriodicItems(bot);
// There should now be no open PRs
var prs = author.openPullRequests();
assertEquals(0, prs.size());
// The author is persistent
pr.setState(Issue.State.OPEN);
prs = author.openPullRequests();
assertEquals(1, prs.size());
// But so is the bot
TestBotRunner.runPeriodicItems(bot);
prs = author.openPullRequests();
assertEquals(0, prs.size());
// There should still only be one welcome comment
assertEquals(1, pr.comments().size());
// The message should mention mirroring
assertTrue(pr.comments().get(0).body().contains("This repository is currently a read-only git mirror"));
}
}
@Test
void dataMessage(TestInfo testInfo) throws IOException {
try (var credentials = new HostCredentials(testInfo);
var tempFolder = new TemporaryDirectory()) {
var author = credentials.getHostedRepository();
var bot = new PullRequestCloserBot(author, PullRequestCloserBot.Type.DATA);
// Populate the projects repository
var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
var masterHash = localRepo.resolve("master").orElseThrow();
localRepo.push(masterHash, author.authenticatedUrl(), "master", true);
// Make a change with a corresponding PR
var editHash = CheckableRepository.appendAndCommit(localRepo);
localRepo.push(editHash, author.authenticatedUrl(), "edit", true);
var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
// Let the bot see it
TestBotRunner.runPeriodicItems(bot);
// There should now be no open PRs
var prs = author.openPullRequests();
assertEquals(0, prs.size());
// The author is persistent
pr.setState(Issue.State.OPEN);
prs = author.openPullRequests();
assertEquals(1, prs.size());
// But so is the bot
TestBotRunner.runPeriodicItems(bot);
prs = author.openPullRequests();
assertEquals(0, prs.size());
// There should still only be one welcome comment
assertEquals(1, pr.comments().size());
// The message should mention automatically generated data
assertTrue(pr.comments().get(0).body().contains("This repository currently holds only automatically generated data"));
}
}
}
================================================
FILE: bots/bridgekeeper/src/test/java/org/openjdk/skara/bots/bridgekeeper/PullRequestPrunerBotTests.java
================================================
/*
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.bridgekeeper;
import org.openjdk.skara.issuetracker.Issue;
import org.openjdk.skara.test.*;
import org.junit.jupiter.api.*;
import java.io.IOException;
import java.time.Duration;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
class PullRequestPrunerBotTests {
@Test
void close(TestInfo testInfo) throws IOException, InterruptedException {
try (var credentials = new HostCredentials(testInfo);
var tempFolder = new TemporaryDirectory()) {
var author = credentials.getHostedRepository();
var ignoredUser = credentials.getHostedRepository();
var bot = new PullRequestPrunerBot(Map.of(author, Duration.ofMillis(1)), Set.of("user2"));
// Populate the projects repository
var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
var masterHash = localRepo.resolve("master").orElseThrow();
localRepo.push(masterHash, author.authenticatedUrl(), "master", true);
// Make a change with a corresponding PR
var editHash = CheckableRepository.appendAndCommit(localRepo);
localRepo.push(editHash, author.authenticatedUrl(), "edit", true);
var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
var ignoredUserPr = ignoredUser.pullRequest(pr.id());
// Make sure the timeout expires
Thread.sleep(100);
// Let the bot see it - it should give a notice
TestBotRunner.runPeriodicItems(bot);
assertEquals(1, pr.comments().size());
assertTrue(pr.comments().get(0).body().contains("will be automatically closed if"));
pr.addComment("I'm still working on it!");
// Make sure the timeout expires again
Thread.sleep(100);
// Let the bot see it - it should post a second notice
TestBotRunner.runPeriodicItems(bot);
assertEquals(3, pr.comments().size());
assertTrue(pr.comments().get(2).body().contains("will be automatically closed if"));
// Add a commit to the pr
var editHash2 = CheckableRepository.appendAndCommit(localRepo);
localRepo.push(editHash2, author.authenticatedUrl(), "edit", true);
TestBotRunner.runPeriodicItems(bot);
// Make sure the timeout expires again
Thread.sleep(100);
TestBotRunner.runPeriodicItems(bot);
assertEquals(Issue.State.OPEN, pr.store().state());
assertEquals(4, pr.comments().size());
assertTrue(pr.comments().get(3).body().contains("will be automatically closed if"));
pr.makeDraft();
// Make sure the timeout expires again
Thread.sleep(100);
TestBotRunner.runPeriodicItems(bot);
assertEquals(Issue.State.OPEN, pr.store().state());
assertEquals(5, pr.comments().size());
assertTrue(pr.comments().get(4).body().contains("will be automatically closed if"));
pr.makeNotDraft();
// Make sure the timeout expires again
Thread.sleep(100);
TestBotRunner.runPeriodicItems(bot);
assertEquals(Issue.State.OPEN, pr.store().state());
assertEquals(6, pr.comments().size());
assertTrue(pr.comments().get(5).body().contains("will be automatically closed if"));
// Post a comment as ignored User
ignoredUserPr.addComment("It should be ignored");
// Make sure the timeout expires again
Thread.sleep(100);
// The bot should now close it
TestBotRunner.runPeriodicItems(bot);
// There should now be no open PRs
var prs = author.openPullRequests();
assertEquals(0, prs.size());
// There should be a mention on how to reopen
var comment = pr.comments().getLast().body();
assertTrue(comment.contains("`/open`"), comment);
}
}
@Test
void dontClose(TestInfo testInfo) throws IOException {
try (var credentials = new HostCredentials(testInfo);
var tempFolder = new TemporaryDirectory()) {
var author = credentials.getHostedRepository();
var bot = new PullRequestPrunerBot(Map.of(author, Duration.ofDays(3)), Set.of());
// Populate the projects repository
var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
var masterHash = localRepo.resolve("master").orElseThrow();
localRepo.push(masterHash, author.authenticatedUrl(), "master", true);
// Make a change with a corresponding PR
var editHash = CheckableRepository.appendAndCommit(localRepo);
localRepo.push(editHash, author.authenticatedUrl(), "edit", true);
var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
// Let the bot see it
TestBotRunner.runPeriodicItems(bot);
// There should still be an open PR
var prs = author.openPullRequests();
assertEquals(1, prs.size());
}
}
}
================================================
FILE: bots/censussync/build.gradle
================================================
/*
* Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module {
name = 'org.openjdk.skara.bots.censussync'
test {
requires 'org.junit.jupiter.api'
requires 'org.openjdk.skara.test'
opens 'org.openjdk.skara.bots.censussync' to 'org.junit.platform.commons'
}
}
dependencies {
implementation project(':bot')
implementation project(':ci')
implementation project(':vcs')
implementation project(':host')
implementation project(':forge')
implementation project(':issuetracker')
implementation project(':census')
implementation project(':process')
implementation project(':json')
implementation project(':network')
implementation project(':storage')
implementation project(':xml')
implementation project(':metrics')
implementation project(':bots:common')
implementation project(':jbs')
implementation project(':jcheck')
testImplementation project(':test')
}
================================================
FILE: bots/censussync/src/main/java/module-info.java
================================================
/*
* Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module org.openjdk.skara.bots.censussync {
requires org.openjdk.skara.vcs;
requires org.openjdk.skara.host;
requires org.openjdk.skara.network;
requires org.openjdk.skara.bot;
requires org.openjdk.skara.process;
requires org.openjdk.skara.storage;
requires org.openjdk.skara.xml;
requires java.logging;
requires java.xml;
requires java.net.http;
requires org.openjdk.skara.jcheck;
requires org.openjdk.skara.jbs;
requires org.openjdk.skara.bots.common;
provides org.openjdk.skara.bot.BotFactory with org.openjdk.skara.bots.censussync.CensusSyncBotFactory;
}
================================================
FILE: bots/censussync/src/main/java/org/openjdk/skara/bots/censussync/CensusSyncBotFactory.java
================================================
/*
* Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.censussync;
import org.openjdk.skara.bot.*;
import java.net.URI;
import java.util.*;
import java.util.logging.Logger;
public class CensusSyncBotFactory implements BotFactory {
private static final Logger log = Logger.getLogger("org.openjdk.skara.bots");
static final String NAME = "censussync";
@Override
public String name() {
return NAME;
}
@Override
public List create(BotConfiguration configuration) {
var bots = new ArrayList();
var specific = configuration.specific();
for (var sync : specific.get("sync").asArray()) {
switch (sync.get("method").asString()) {
case "unify" -> {
var from = configuration.repository(sync.get("from").asString());
var to = configuration.repository(sync.get("to").asString());
var version = sync.get("version").asInt();
bots.add(new CensusSyncUnifyBot(from, to, version));
}
case "split" -> {
var from = URI.create(sync.get("from").asString());
var to = configuration.repository(sync.get("to").asString());
var version = sync.get("version").asInt();
bots.add(new CensusSyncSplitBot(from, to, version));
}
}
}
return bots;
}
}
================================================
FILE: bots/censussync/src/main/java/org/openjdk/skara/bots/censussync/CensusSyncSplitBot.java
================================================
/*
* Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.censussync;
import org.openjdk.skara.bot.*;
import org.openjdk.skara.bots.common.BotUtils;
import org.openjdk.skara.forge.HostedRepository;
import org.openjdk.skara.network.RestRequest;
import org.openjdk.skara.vcs.*;
import org.openjdk.skara.xml.XML;
import org.w3c.dom.Element;
import java.io.*;
import java.net.URI;
import java.nio.file.*;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.logging.Logger;
public class CensusSyncSplitBot implements Bot, WorkItem {
private static final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
private final URI from;
private final HostedRepository to;
private final int version;
private final RestRequest request;
private String lastCensus = "";
CensusSyncSplitBot(URI from, HostedRepository to, int version) {
this.from = from;
this.to = to;
this.version = version;
request = new RestRequest(from);
}
@Override
public boolean concurrentWith(WorkItem other) {
if (!(other instanceof CensusSyncSplitBot o)) {
return true;
}
return !o.to.equals(to);
}
@Override
public String toString() {
return "CensusSyncSplitBot(" + from + "->" + to.name() + "@" + version + ")";
}
@Override
public List getPeriodicItems() {
return List.of(this);
}
private static PrintWriter newPrintWriter(Path p) throws IOException {
return new PrintWriter(Files.newBufferedWriter(p));
}
private static List syncVersion(Element census, Path to) throws IOException {
var date = ZonedDateTime.parse(XML.attribute(census, "time"));
var timestamp = date.toInstant();
var filename = to.resolve("version.xml");
try (var file = newPrintWriter(filename)) {
file.println("");
file.format("%n", timestamp.toString());
}
return List.of(filename);
}
private static List syncContributors(Element census, Path to) throws IOException {
var filename = to.resolve("contributors.xml");
try (var file = newPrintWriter(filename)) {
file.println("");
file.println("");
for (var person : XML.children(census, "person")) {
var username = XML.attribute(person, "name");
var fullName = XML.child(person, "full-name").getTextContent();
file.format(" %n",
username, fullName);
}
file.println("");
}
return List.of(filename);
}
private static List syncGroups(Element census, Path to) throws IOException {
var dir = to.resolve("groups");
var ret = new ArrayList();
for (var group : XML.children(census, "group")) {
Files.createDirectories(dir);
String lead = null;
var members = new ArrayList();
for (var person : XML.children(group, "person")) {
if (XML.hasAttribute(person, "role")) {
var role = XML.attribute(person, "role");
if (!role.equals("lead")) {
throw new IOException("Unexpected role: " + role);
}
lead = XML.attribute(person, "ref");
} else {
members.add(XML.attribute(person, "ref"));
}
}
var name = XML.attribute(group, "name");
var fullName = XML.child(group, "full-name").getTextContent();
var filename = dir.resolve(name + ".xml");
try (var file = newPrintWriter(filename)) {
file.format("%n");
file.format("%n", name, BotUtils.escape(fullName));
file.format(" %n", lead);
for (var member : members) {
file.format(" %n", member);
}
file.format("%n");
}
ret.add(filename);
}
return ret;
}
private static List syncProjects(Element census, Path to) throws IOException {
var dir = to.resolve("projects");
var ret = new ArrayList();
for (var project : XML.children(census, "project")) {
Files.createDirectories(dir);
String lead = null;
var committers = new ArrayList();
var reviewers = new ArrayList();
var authors = new ArrayList();
var name = XML.attribute(project, "name");
for (var person : XML.children(project, "person")) {
var role = XML.attribute(person, "role");
var username = XML.attribute(person, "ref");
switch (role) {
case "lead":
lead = username;
break;
case "reviewer":
reviewers.add(username);
break;
case "committer":
committers.add(username);
break;
case "author":
authors.add(username);
break;
default:
if (name.equals("openjfx") && (username.equals("dwookey") || username.equals("jpereda"))) {
authors.add(username);
} else {
throw new IOException("Unexpected role '" + role +
"' for user '" + username +
"' in project '" + name + "'");
}
}
}
var fullName = XML.child(project, "full-name").getTextContent();
var sponsor = XML.attribute(XML.child(project, "sponsor"), "ref");
var filename = dir.resolve(name + ".xml");
try (var file = newPrintWriter(filename)) {
file.format("%n");
file.format("%n", name, BotUtils.escape(fullName), sponsor);
file.format(" %n", lead);
for (var reviewer : reviewers) {
file.format(" %n", reviewer);
}
for (var committer : committers) {
file.format(" %n", committer);
}
for (var author : authors) {
file.format(" %n", author);
}
file.format("%n");
}
ret.add(filename);
}
return ret;
}
private static List sync(String from, Path to) throws IOException {
var document = XML.parse(from);
var census = XML.child(document, "census");
var ret = new ArrayList();
ret.addAll(syncVersion(census, to));
ret.addAll(syncContributors(census, to));
ret.addAll(syncGroups(census, to));
ret.addAll(syncProjects(census, to));
return ret;
}
@Override
public Collection run(Path scratch) {
try {
var currentCensus = request.get().executeUnparsed();
if (currentCensus.equals(lastCensus)) {
log.fine("No census changes detected");
return List.of();
}
var toDir = scratch.resolve("to.git");
var toRepo = Repository.materialize(toDir, to.authenticatedUrl(), Branch.defaultFor(VCS.GIT).name());
var updatedFiles = sync(currentCensus, toDir);
if (!toRepo.isClean()) {
toRepo.add(updatedFiles);
var head = toRepo.commit("Updated census", "duke", "duke@openjdk.org");
toRepo.push(head, to.authenticatedUrl(), Branch.defaultFor(VCS.GIT).name(), false);
} else {
log.info("New census data did not result in any changes");
}
lastCensus = currentCensus;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return List.of();
}
@Override
public String name() {
return CensusSyncBotFactory.NAME;
}
@Override
public String botName() {
return name();
}
@Override
public String workItemName() {
return "split";
}
}
================================================
FILE: bots/censussync/src/main/java/org/openjdk/skara/bots/censussync/CensusSyncUnifyBot.java
================================================
/*
* Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.censussync;
import org.openjdk.skara.bot.*;
import org.openjdk.skara.bots.common.BotUtils;
import org.openjdk.skara.census.Census;
import org.openjdk.skara.forge.*;
import org.openjdk.skara.vcs.*;
import java.io.*;
import java.util.*;
import java.nio.file.*;
import java.util.logging.Logger;
import java.time.*;
import java.time.format.*;
public class CensusSyncUnifyBot implements Bot, WorkItem {
private static final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
private final HostedRepository from;
private final HostedRepository to;
private final int version;
private Hash last;
CensusSyncUnifyBot(HostedRepository from, HostedRepository to, int version) {
this.from = from;
this.to = to;
this.version = version;
this.last = null;
}
@Override
public boolean concurrentWith(WorkItem other) {
if (!(other instanceof CensusSyncUnifyBot o)) {
return true;
}
return !o.to.equals(to);
}
@Override
public String toString() {
return "CensusSyncUnifyBot(" + from.name() + "->" + to.name() + "@" + version + ")";
}
@Override
public List getPeriodicItems() {
return List.of(this);
}
@Override
public Collection run(Path scratch) {
try {
var fromDir = scratch.resolve("from.git");
var fromRepo = Repository.materialize(fromDir, from.authenticatedUrl(), Branch.defaultFor(VCS.GIT).name());
if (last != null && last.equals(fromRepo.head())) {
// Nothing to do
return List.of();
}
var census = Census.parse(fromDir);
var toDir = scratch.resolve("to.git");
var toRepo = Repository.materialize(toDir, to.authenticatedUrl(), Branch.defaultFor(VCS.GIT).name());
var censusXML = toRepo.root().resolve("census.xml");
if (!Files.exists(censusXML)) {
Files.createFile(censusXML);
}
try (var file = new PrintWriter(Files.newBufferedWriter(censusXML))) {
file.println("");
var formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
file.println("");
for (var contributor : census.contributors()) {
file.println("");
file.println(" " + contributor.fullName().orElse("") + "");
file.println("");
}
for (var group : census.groups()) {
file.println("");
file.println(" " + BotUtils.escape(group.fullName()) + "");
file.println(" ");
for (var member : group.members()) {
if (!member.username().equals(group.lead().username())) {
file.println(" ");
}
}
file.println("");
}
for (var project : census.projects()) {
file.println("");
file.println(" " + BotUtils.escape(project.fullName()) + "");
file.println(" ");
var roles = project.roles(version);
for (var role : roles.keySet()) {
for (var member : roles.get(role)) {
file.println(" ");
}
}
file.println("");
}
for (var namespace : census.namespaces()) {
file.println("");
for (var entry : namespace.entries()) {
var id = entry.getKey();
var contributor = entry.getValue();
file.println(" ");
}
file.println("");
}
file.println("");
}
toRepo.add(censusXML);
var head = toRepo.commit("Updated census.xml", "duke", "duke@openjdk.org");
toRepo.push(head, to.authenticatedUrl(), Branch.defaultFor(VCS.GIT).name(), false);
last = fromRepo.head();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return List.of();
}
@Override
public String name() {
return CensusSyncBotFactory.NAME;
}
@Override
public String botName() {
return name();
}
@Override
public String workItemName() {
return "unitfy";
}
}
================================================
FILE: bots/censussync/src/test/java/org/openjdk/skara/bots/censussync/CensusSyncBotFactoryTest.java
================================================
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.censussync;
import org.junit.jupiter.api.Test;
import org.openjdk.skara.json.JWCC;
import org.openjdk.skara.test.TestBotFactory;
import org.openjdk.skara.test.TestHostedRepository;
import static org.junit.jupiter.api.Assertions.*;
class CensusSyncBotFactoryTest {
@Test
void testCreate() {
String jsonString = """
{
"sync": [
{
"method": "unify",
"from": "from1",
"to": "to1",
"version": 1
},
{
"method": "split",
"from": "https://test.org/test.xml",
"to": "to2",
"version": 2
}
]
}
""";
var jsonConfig = JWCC.parse(jsonString).asObject();
var testBotFactory = TestBotFactory.newBuilder()
.addHostedRepository("from1", new TestHostedRepository("from1"))
.addHostedRepository("to1", new TestHostedRepository("to1"))
.addHostedRepository("to2", new TestHostedRepository("to2"))
.build();
var bots = testBotFactory.createBots(CensusSyncBotFactory.NAME, jsonConfig);
assertEquals(2, bots.size());
var censusSyncUnifyBots = bots.stream().filter(e -> e.getClass().equals(CensusSyncUnifyBot.class)).toList();
var censusSyncSplitBots = bots.stream().filter(e -> e.getClass().equals(CensusSyncSplitBot.class)).toList();
assertEquals(1, censusSyncUnifyBots.size());
assertEquals(1, censusSyncSplitBots.size());
assertEquals("CensusSyncUnifyBot(from1->to1@1)", censusSyncUnifyBots.get(0).toString());
assertEquals("CensusSyncSplitBot(https://test.org/test.xml->to2@2)", censusSyncSplitBots.get(0).toString());
}
}
================================================
FILE: bots/checkout/build.gradle
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module {
name = 'org.openjdk.skara.bots.checkout'
test {
requires 'org.junit.jupiter.api'
requires 'org.openjdk.skara.test'
opens 'org.openjdk.skara.bots.checkout' to 'org.junit.platform.commons'
}
}
dependencies {
implementation project(':bot')
implementation project(':ci')
implementation project(':vcs')
implementation project(':host')
implementation project(':forge')
implementation project(':issuetracker')
implementation project(':census')
implementation project(':process')
implementation project(':json')
implementation project(':network')
implementation project(':storage')
implementation project(':metrics')
testImplementation project(':test')
}
================================================
FILE: bots/checkout/src/main/java/module-info.java
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module org.openjdk.skara.bots.checkout {
requires org.openjdk.skara.vcs;
requires org.openjdk.skara.host;
requires org.openjdk.skara.network;
requires org.openjdk.skara.bot;
requires org.openjdk.skara.process;
requires org.openjdk.skara.storage;
requires java.logging;
provides org.openjdk.skara.bot.BotFactory with org.openjdk.skara.bots.checkout.CheckoutBotFactory;
}
================================================
FILE: bots/checkout/src/main/java/org/openjdk/skara/bots/checkout/CheckoutBot.java
================================================
/*
* Copyright (c) 2020, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.checkout;
import org.openjdk.skara.bot.*;
import org.openjdk.skara.forge.HostedRepository;
import org.openjdk.skara.vcs.*;
import org.openjdk.skara.vcs.openjdk.convert.*;
import org.openjdk.skara.storage.StorageBuilder;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.*;
import java.nio.file.*;
import java.nio.charset.StandardCharsets;
import java.net.URI;
import java.net.URLEncoder;
import java.util.logging.Logger;
public class CheckoutBot implements Bot, WorkItem {
private static final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
private final HostedRepository from;
private final Branch branch;
private final Path to;
private final Path storage;
private final StorageBuilder marksStorage;
CheckoutBot(HostedRepository from, Branch branch, Path to, Path storage, StorageBuilder marksStorage) {
this.from = from;
this.branch = branch;
this.to = to;
this.storage = storage;
this.marksStorage = marksStorage;
}
private static String urlEncode(Path p) {
return URLEncoder.encode(p.toString(), StandardCharsets.UTF_8);
}
private static String urlEncode(URI uri) {
return URLEncoder.encode(uri.toString(), StandardCharsets.UTF_8);
}
@Override
public boolean concurrentWith(WorkItem other) {
if (!(other instanceof CheckoutBot o)) {
return true;
}
return !(o.to.equals(to) || o.from.equals(from));
}
@Override
public String toString() {
return "CheckoutBot(" + from.name() + ":" + branch.name() + ", " + to + ")";
}
@Override
public List getPeriodicItems() {
return List.of(this);
}
@Override
public Collection run(Path scratch) {
try {
var fromDir = storage.resolve(urlEncode(from.url()));
Repository fromRepo = null;
if (!Files.exists(fromDir)) {
Files.createDirectories(fromDir);
log.info("Cloning Git repo " + from + " to " + fromDir);
fromRepo = Repository.clone(from.authenticatedUrl(), fromDir);
} else {
log.info("Getting existing Git repo repository from " + fromDir);
fromRepo = Repository.get(fromDir).orElseThrow(() ->
new IllegalStateException("Git repository vanished from " + fromDir));
}
fromRepo.fetchRemote("origin");
fromRepo.checkout(branch);
fromRepo.pull("origin", branch.name(), true);
var toRepoName = to.getFileName().toString();
var marksDir = scratch.resolve("checkout").resolve("marks").resolve(toRepoName);
Files.createDirectories(marksDir);
var marks = marksStorage.materialize(marksDir);
var converter = new GitToHgConverter(branch);
var hasConverted = false;
try {
if (!Files.exists(to)) {
log.info("Creating Hg repository at: " + to);
Files.createDirectories(to);
var toRepo = Repository.init(to, VCS.HG);
converter.convert(fromRepo, toRepo);
hasConverted = true;
} else {
log.info("Found existing Hg repository at: " + to);
var toRepo = Repository.get(to).orElseThrow(() ->
new IllegalStateException("Repository vanished from " + to));
var existing = new ArrayList<>(marks.current());
Collections.sort(existing);
log.info("Found " + existing.size() + " existing marks");
converter.convert(fromRepo, toRepo, existing);
hasConverted = true;
}
} finally {
if (hasConverted) {
log.info("Storing " + converter.marks().size() + " marks");
marks.put(converter.marks());
} else {
log.info("No conversion has taken place, not updating marks");
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return List.of();
}
@Override
public String name() {
return CheckoutBotFactory.NAME;
}
@Override
public String botName() {
return name();
}
@Override
public String workItemName() {
return botName();
}
}
================================================
FILE: bots/checkout/src/main/java/org/openjdk/skara/bots/checkout/CheckoutBotFactory.java
================================================
/*
* Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.checkout;
import org.openjdk.skara.bot.*;
import org.openjdk.skara.vcs.*;
import org.openjdk.skara.vcs.openjdk.convert.Mark;
import java.util.*;
import java.net.URI;
import java.nio.file.Path;
import java.util.logging.Logger;
public class CheckoutBotFactory implements BotFactory {
private static final Logger log = Logger.getLogger("org.openjdk.skara.bots");
static final String NAME = "checkout";
@Override
public String name() {
return NAME;
}
@Override
public List create(BotConfiguration configuration) {
var specific = configuration.specific();
var storage = configuration.storageFolder();
var marksRepo = configuration.repository(specific.get("marks").get("repo").asString());
var marksUser = Author.fromString(specific.get("marks").get("author").asString());
var bots = new ArrayList();
for (var repo : specific.get("repositories").asArray()) {
var from = configuration.repository(repo.get("from").get("repo").asString());
var fromBranch = new Branch(repo.get("from").get("branch").asString());
var to = Path.of(repo.get("to").asString());
var toRepoName = to.getFileName().toString();
var markStorage = MarkStorage.create(marksRepo, marksUser, toRepoName);
bots.add(new CheckoutBot(from, fromBranch, to, storage, markStorage));
}
return bots;
}
}
================================================
FILE: bots/checkout/src/main/java/org/openjdk/skara/bots/checkout/MarkStorage.java
================================================
/*
* Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.checkout;
import org.openjdk.skara.forge.HostedRepository;
import org.openjdk.skara.vcs.*;
import org.openjdk.skara.vcs.openjdk.convert.Mark;
import org.openjdk.skara.storage.StorageBuilder;
import java.net.URI;
import java.util.*;
import java.util.stream.Collectors;
class MarkStorage {
private static Mark deserializeMark(String s) {
var parts = s.split(" ");
if (!(parts.length == 3 || parts.length == 4)) {
throw new IllegalArgumentException("Unexpected string:" + s);
}
var key = Integer.parseInt(parts[0]);
var hg = new Hash(parts[1]);
var git = new Hash(parts[2]);
return parts.length == 3 ? new Mark(key, hg, git) : new Mark(key, hg, git, new Hash(parts[3]));
}
private static String serialize(Collection added, Set existing) {
var marks = new ArrayList();
var handled = new HashSet();
for (var mark : added) {
marks.add(mark);
handled.add(mark.key());
}
for (var mark : existing) {
if (!handled.contains(mark.key())) {
marks.add(mark);
}
}
Collections.sort(marks);
var sb = new StringBuilder();
for (var mark : marks) {
sb.append(Integer.toString(mark.key()));
sb.append(" ");
sb.append(mark.hg().hex());
sb.append(" ");
sb.append(mark.git().hex());
if (mark.tag().isPresent()) {
sb.append(" ");
sb.append(mark.tag().get().hex());
}
sb.append("\n");
}
return sb.toString();
}
private static Set deserialize(String current) {
var res = current.lines()
.map(MarkStorage::deserializeMark)
.collect(Collectors.toSet());
return res;
}
static StorageBuilder create(HostedRepository repo, Author user, String name) {
return new StorageBuilder(name + "/marks.txt")
.remoteRepository(repo, Branch.defaultFor(VCS.GIT).name(), user.name(), user.email(), "Updated marks for " + name)
.serializer(MarkStorage::serialize)
.deserializer(MarkStorage::deserialize);
}
}
================================================
FILE: bots/checkout/src/test/java/org/openjdk/skara/bots/checkout/CheckoutBotFactoryTest.java
================================================
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.checkout;
import org.junit.jupiter.api.Test;
import org.openjdk.skara.json.JWCC;
import org.openjdk.skara.test.TestBotFactory;
import org.openjdk.skara.test.TestHostedRepository;
import static org.junit.jupiter.api.Assertions.*;
class CheckoutBotFactoryTest {
@Test
public void testCreate() {
String jsonString = """
{
"marks": {
"repo": "mark",
"author": "test_author "
},
"repositories": [
{
"from": {
"repo": "from1",
"branch": "master"
},
"to": "to1"
},
{
"from": {
"repo": "from2",
"branch": "dev"
},
"to": "to2"
}
]
}
""";
var jsonConfig = JWCC.parse(jsonString).asObject();
var testBotFactory = TestBotFactory.newBuilder()
.addHostedRepository("mark", new TestHostedRepository("mark"))
.addHostedRepository("from1", new TestHostedRepository("from1"))
.addHostedRepository("from2", new TestHostedRepository("from2"))
.build();
var bots = testBotFactory.createBots(CheckoutBotFactory.NAME, jsonConfig);
// A checkoutBot for every configured repository
assertEquals(2, bots.size());
assertEquals("CheckoutBot(from1:master, to1)", bots.get(0).toString());
assertEquals("CheckoutBot(from2:dev, to2)", bots.get(1).toString());
}
}
================================================
FILE: bots/checkout/src/test/java/org/openjdk/skara/bots/checkout/CheckoutBotTests.java
================================================
/*
* Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.checkout;
import org.openjdk.skara.test.*;
import org.openjdk.skara.host.HostUser;
import org.openjdk.skara.vcs.*;
import org.junit.jupiter.api.*;
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import static java.nio.file.StandardOpenOption.*;
import static org.junit.jupiter.api.Assertions.*;
class CheckoutBotTests {
private static void populate(Repository r) throws IOException {
var readme = r.root().resolve("README");
Files.write(readme, List.of("Hello, readme!"));
r.add(readme);
r.commit("Add README", "duke", "duke@openjdk.org");
Files.write(readme, List.of("Another line"), WRITE, APPEND);
r.add(readme);
r.commit("Modify README", "duke", "duke@openjdk.org");
Files.write(readme, List.of("A final line"), WRITE, APPEND);
r.add(readme);
r.commit("Final README", "duke", "duke@openjdk.org");
}
@Test
void simpleConversion(TestInfo testInfo) throws IOException {
try (var tmp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var marksLocalDir = tmp.path().resolve("marks.git");
Files.createDirectories(marksLocalDir);
var marksLocalRepo = TestableRepository.init(marksLocalDir, VCS.GIT);
marksLocalRepo.config("receive", "denyCurrentBranch", "ignore");
var marksHostedRepo = new TestHostedRepository(host, "marks", marksLocalRepo);
var storage = tmp.path().resolve("storage");
var scratch = tmp.path().resolve("scratch");
var marksAuthor = new Author("duke", "duke@openjdk.org");
var marksStorage = MarkStorage.create(marksHostedRepo, marksAuthor, "test");
var hgDir = tmp.path().resolve("hg");
var gitLocalDir = tmp.path().resolve("from.git");
Files.createDirectories(gitLocalDir);
var gitLocalRepo = TestableRepository.init(gitLocalDir, VCS.GIT);
populate(gitLocalRepo);
var gitHostedRepo = new TestHostedRepository(host, "from", gitLocalRepo);
var bot = new CheckoutBot(gitHostedRepo, gitLocalRepo.defaultBranch(), hgDir, storage, marksStorage);
var runner = new TestBotRunner();
runner.runPeriodicItems(bot);
var hgRepo = Repository.get(hgDir).orElseThrow();
assertEquals(3, hgRepo.commitMetadata().size());
}
}
@Test
void update(TestInfo testInfo) throws IOException {
try (var tmp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var marksLocalDir = tmp.path().resolve("marks.git");
Files.createDirectories(marksLocalDir);
var marksLocalRepo = TestableRepository.init(marksLocalDir, VCS.GIT);
marksLocalRepo.config("receive", "denyCurrentBranch", "ignore");
var marksHostedRepo = new TestHostedRepository(host, "marks", marksLocalRepo);
var storage = tmp.path().resolve("storage");
var scratch = tmp.path().resolve("scratch");
var marksAuthor = new Author("duke", "duke@openjdk.org");
var marksStorage = MarkStorage.create(marksHostedRepo, marksAuthor, "test");
var runner = new TestBotRunner();
var hgDir = tmp.path().resolve("hg");
var gitLocalDir = tmp.path().resolve("from.git");
Files.createDirectories(gitLocalDir);
var gitLocalRepo = TestableRepository.init(gitLocalDir, VCS.GIT);
populate(gitLocalRepo);
var gitHostedRepo = new TestHostedRepository(host, "from", gitLocalRepo);
var bot = new CheckoutBot(gitHostedRepo, gitLocalRepo.defaultBranch(), hgDir, storage, marksStorage);
runner.runPeriodicItems(bot);
var hgRepo = Repository.get(hgDir).orElseThrow();
assertEquals(3, hgRepo.commitMetadata().size());
assertEquals(3, gitLocalRepo.commitMetadata().size());
var readme = gitLocalRepo.root().resolve("README");
Files.write(readme, List.of("An updated line"), WRITE, APPEND);
gitLocalRepo.add(readme);
gitLocalRepo.commit("Updated Final README", "duke", "duke@openjdk.org");
runner.runPeriodicItems(bot);
assertEquals(4, hgRepo.commitMetadata().size());
}
}
}
================================================
FILE: bots/cli/build.gradle
================================================
/*
* Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
plugins {
id 'skara-images'
}
module {
name = 'org.openjdk.skara.bots.cli'
test {
requires 'org.junit.jupiter.api'
requires 'org.openjdk.skara.test'
requires 'jdk.httpserver'
opens 'org.openjdk.skara.bots.cli' to 'org.junit.platform.commons'
}
}
dependencies {
implementation project(':bots:pr')
implementation project(':bots:hgbridge')
implementation project(':bots:forward')
implementation project(':bots:notify')
implementation project(':bots:merge')
implementation project(':bots:mlbridge')
implementation project(':bots:mirror')
implementation project(':bots:topological')
implementation project(':bots:tester')
implementation project(':bots:submit')
implementation project(':bots:forward')
implementation project(':bots:bridgekeeper')
implementation project(':bots:checkout')
implementation project(':bots:censussync')
implementation project(':bots:testinfo')
implementation project(':bots:synclabel')
implementation project(':ci')
implementation project(':vcs')
implementation project(':jcheck')
implementation project(':host')
implementation project(':network')
implementation project(':bot')
implementation project(':forge')
implementation project(':issuetracker')
implementation project(':census')
implementation project(':json')
implementation project(':ini')
implementation project(':process')
implementation project(':args')
implementation project(':proxy')
implementation project(':version')
implementation project(':metrics')
testImplementation project(':test')
}
// Load deps.env and remove all double quotes
def depsEnv = new Properties()
file("../../deps.env").withInputStream { { depsEnv.load(it)}}
depsEnv.entrySet().forEach(e -> e.setValue(((String) e.getValue()).replaceAll("\"", "")))
images {
linux_x64 {
modules = ['jdk.crypto.ec',
'org.openjdk.skara.bots.pr',
'org.openjdk.skara.bots.hgbridge',
'org.openjdk.skara.bots.forward',
'org.openjdk.skara.bots.notify',
'org.openjdk.skara.bots.merge',
'org.openjdk.skara.bots.mlbridge',
'org.openjdk.skara.bots.mirror',
'org.openjdk.skara.bots.submit',
'org.openjdk.skara.bots.tester',
'org.openjdk.skara.bots.topological',
'org.openjdk.skara.bots.forward',
'org.openjdk.skara.bots.bridgekeeper',
'org.openjdk.skara.bots.checkout',
'org.openjdk.skara.bots.censussync',
'org.openjdk.skara.bots.testinfo',
'org.openjdk.skara.bots.synclabel']
launchers = ['skara-bots': 'org.openjdk.skara.bots.cli/org.openjdk.skara.bots.cli.BotLauncher']
options = ["--module-path", "plugins"]
bundles = ['zip', 'tar.gz']
jdk {
url = depsEnv.getProperty("JDK_LINUX_X64_URL")
sha256 = depsEnv.getProperty("JDK_LINUX_X64_SHA256")
}
}
}
================================================
FILE: bots/cli/src/main/java/module-info.java
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module org.openjdk.skara.bots.cli {
requires org.openjdk.skara.vcs;
requires org.openjdk.skara.jcheck;
requires org.openjdk.skara.host;
requires org.openjdk.skara.bot;
requires org.openjdk.skara.census;
requires org.openjdk.skara.json;
requires org.openjdk.skara.args;
requires org.openjdk.skara.process;
requires org.openjdk.skara.proxy;
requires org.openjdk.skara.network;
requires org.openjdk.skara.version;
requires java.net.http;
requires java.sql;
exports org.openjdk.skara.bots.cli;
}
================================================
FILE: bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotConsoleHandler.java
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.cli;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.logging.*;
class BotConsoleHandler extends StreamHandler {
private final DateTimeFormatter dateTimeFormatter;
private final Map levelAbbreviations;
BotConsoleHandler() {
dateTimeFormatter = DateTimeFormatter.ISO_INSTANT
.withLocale(Locale.getDefault())
.withZone(ZoneId.systemDefault());
levelAbbreviations = new HashMap<>();
levelAbbreviations.put(Level.INFO.intValue(), "I");
levelAbbreviations.put(Level.FINE.intValue(), "F");
levelAbbreviations.put(Level.FINER.intValue(), "finer");
levelAbbreviations.put(Level.FINEST.intValue(), "finest");
levelAbbreviations.put(Level.SEVERE.intValue(), "E");
levelAbbreviations.put(Level.WARNING.intValue(), "W");
}
@Override
public void publish(LogRecord record) {
if (record.getLevel().intValue() < getLevel().intValue()) {
return;
}
var level = levelAbbreviations.getOrDefault(record.getLevel().intValue(), "?");
System.out.println("[" + dateTimeFormatter.format(record.getInstant().truncatedTo(ChronoUnit.SECONDS)) + "][" + record.getLongThreadID() + "][" +
level + "] " + record.getMessage());
var exception = record.getThrown();
if (exception != null) {
exception.printStackTrace(System.out);
}
System.out.flush();
}
}
================================================
FILE: bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLauncher.java
================================================
/*
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.cli;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import org.openjdk.skara.args.*;
import org.openjdk.skara.bot.*;
import org.openjdk.skara.network.URIBuilder;
import org.openjdk.skara.json.*;
import org.openjdk.skara.proxy.HttpProxy;
import org.openjdk.skara.version.Version;
import java.io.IOException;
import java.nio.file.*;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.logging.*;
import java.util.stream.*;
public class BotLauncher {
private static Logger log;
private static final Instant START_TIME = Instant.now();
private static void applyLogging(JSONObject config) {
LogManager.getLogManager().reset();
log = Logger.getLogger("org.openjdk");
log.setLevel(Level.FINEST);
if (!config.contains("log")) {
return;
}
if (config.get("log").asObject().contains("console")) {
var level = Level.parse(config.get("log").get("console").get("level").asString());
var handler = new BotConsoleHandler();
handler.setLevel(level);
log.addHandler(handler);
}
if (config.get("log").asObject().contains("slack")) {
var maxRate = Duration.ofMinutes(10);
JSONValue slack = config.get("log").get("slack");
if (slack.asObject().contains("maxrate")) {
maxRate = Duration.parse(slack.get("maxrate").asString());
}
var level = Level.parse(slack.get("level").asString());
Map details = new HashMap<>();
if (slack.asObject().contains("details")) {
details = slack.get("details").asArray().stream()
.collect(Collectors.toMap(o -> o.get("pattern").asString(),
o -> o.get("link").asString()));
}
var username = slack.get("username");
var prefix = slack.get("prefix");
var handler = new BotSlackHandler(URIBuilder.base(slack.get("webhook").asString()).build(),
username == null ? null : username.asString(),
prefix == null ? null : prefix.asString(),
maxRate,
details);
handler.setLevel(level);
log.addHandler(handler);
}
if (config.get("log").asObject().contains("logstash")) {
var logstashConf = config.get("log").get("logstash").asObject();
var level = Level.parse(logstashConf.get("level").asString());
var handler = new BotLogstashHandler(URIBuilder.base(logstashConf.get("endpoint").asString()).build());
if (logstashConf.contains("fields")) {
for (var field : logstashConf.get("fields").asArray()) {
if (field.asObject().contains("pattern")) {
handler.addExtraField(field.get("name").asString(),
field.get("value").asString(),
field.get("pattern").asString());
} else {
handler.addExtraField(field.get("name").asString(),
field.get("value").asString());
}
}
}
if (logstashConf.contains("replacements")) {
for (var field : logstashConf.get("replacements").asArray()) {
handler.addReplacement(field.get("pattern").asString(), field.get("replacement").asString());
}
}
handler.setLevel(level);
var dateTimeFormatter = DateTimeFormatter.ISO_INSTANT
.withLocale(Locale.getDefault())
.withZone(ZoneId.systemDefault());
handler.addExtraField("instance_start_time", dateTimeFormatter.format(START_TIME));
log.addHandler(handler);
}
}
private static JSONObject readConfiguration(Path jsonFile) {
try {
return JWCC.parse(Files.readString(jsonFile)).asObject();
} catch (IOException e) {
throw new RuntimeException("Failed to open configuration file: " + jsonFile);
}
}
public static void main(String... args) {
HttpProxy.setup();
var flags = List.of(
Option.shortcut("t")
.fullname("timeout")
.describe("ISO8601")
.helptext("When running once, only run for this long (default 1 hour)")
.optional(),
Switch.shortcut("o")
.fullname("once")
.helptext("Instead of repeatedly executing periodical task, run each task exactly once")
.optional(),
Switch.shortcut("v")
.fullname("version")
.helptext("Show version")
.optional(),
Switch.shortcut("l")
.fullname("list-bots")
.helptext("List all available bots and then exit")
.optional());
var inputs = List.of(
Input.position(0)
.describe("configuration.json")
.singular()
.required());
var parser = new ArgumentParser("bots", flags, inputs);
var arguments = parser.parse(args);
if (arguments.contains("list-bots")) {
var botFactories = BotFactory.getBotFactories();
System.out.println("Number of available bots: " + botFactories.size());
for (var botFactory : botFactories) {
System.out.println(" - " + botFactory.name() + " (" + botFactory.getClass().getModule() + ")");
}
System.exit(0);
}
if (arguments.contains("version")) {
System.out.println(Version.fromManifest().orElse("unknown"));
System.exit(0);
}
Path jsonFile = arguments.at(0).via(Paths::get);
var jsonConfig = readConfiguration(jsonFile);
applyLogging(jsonConfig);
var log = Logger.getLogger("org.openjdk.skara.bots.cli");
log.info("Starting BotLauncher");
BotRunnerConfiguration runnerConfig = null;
try {
runnerConfig = BotRunnerConfiguration.parse(jsonConfig, jsonFile.getParent());
} catch (ConfigurationError configurationError) {
log.severe("Failed to parse configuration file: " + jsonFile
+ " error message: " + configurationError.getMessage());
// Also print directly as logging may not be setup
System.out.println("Failed to parse configuration file: " + jsonFile);
System.out.println("Error message: " + configurationError.getMessage());
System.exit(1);
}
var botFactories = BotFactory.getBotFactories().stream()
.collect(Collectors.toMap(BotFactory::name, Function.identity()));
if (botFactories.size() == 0) {
log.severe("Error: no bot factories found. Make sure the module path is correct. Exiting...");
// Also print directly as logging may not be setup
System.out.println("Error: no bot factories found. Make sure the module path is correct. Exiting...");
System.exit(1);
}
var bots = new ArrayList();
for (var botEntry : botFactories.entrySet()) {
try {
var botConfig = runnerConfig.perBotConfiguration(botEntry.getKey());
bots.addAll(botEntry.getValue().create(botConfig));
} catch (ConfigurationError configurationError) {
log.info("No configuration for available bot '" + botEntry.getKey() + "', skipping...");
}
}
var runner = new BotRunner(runnerConfig, bots);
try {
if (arguments.contains("once")) {
runner.runOnce(arguments.get("timeout").or("PT60M").via(Duration::parse));
} else {
runner.run();
}
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
================================================
FILE: bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLogstashHandler.java
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.cli;
import org.openjdk.skara.bot.LogContextMap;
import org.openjdk.skara.json.JSON;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URI;
import java.net.http.*;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.Future;
import java.util.logging.*;
import java.util.regex.Pattern;
/**
* Handles logging to logstash. Be careful not to call anything that creates new
* log records from this class as that can cause infinite recursion.
*/
public class BotLogstashHandler extends StreamHandler {
private final URI endpoint;
private final HttpClient httpClient;
private final DateTimeFormatter dateTimeFormatter;
// Optionally store all futures for testing purposes
private Collection>> futures;
private record RegexReplacement(Pattern pattern, String replacement) {}
private final List regexReplacements = new ArrayList<>();
private static class ExtraField {
String name;
String value;
Pattern pattern;
}
private final List extraFields;
BotLogstashHandler(URI endpoint) {
this.endpoint = endpoint;
this.httpClient = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(30))
.build();
dateTimeFormatter = DateTimeFormatter.ISO_INSTANT
.withLocale(Locale.getDefault())
.withZone(ZoneId.systemDefault());
extraFields = new ArrayList<>();
}
void addExtraField(String name, String value) {
addExtraField(name, value, null);
}
void addExtraField(String name, String value, String pattern) {
var extraField = new ExtraField();
extraField.name = name;
extraField.value = value;
if (pattern != null) {
extraField.pattern = Pattern.compile(pattern);
}
extraFields.add(extraField);
}
void addReplacement(String pattern, String replacement) {
regexReplacements.add(new RegexReplacement(Pattern.compile(pattern), replacement));
}
private Map getExtraFields(LogRecord record) {
var ret = new HashMap();
for (var extraField : extraFields) {
if (extraField.pattern != null) {
var matcher = extraField.pattern.matcher(record.getMessage());
if (matcher.matches()) {
var value = matcher.replaceFirst(extraField.value);
ret.put(extraField.name, value);
}
} else {
ret.put(extraField.name, extraField.value);
}
}
return ret;
}
private String applyReplacements(String s) {
CharSequence ret = s;
for (RegexReplacement regexReplacement : regexReplacements) {
var matcher = regexReplacement.pattern.matcher(ret);
var sb = new StringBuilder();
while (matcher.find()) {
matcher.appendReplacement(sb, regexReplacement.replacement);
}
matcher.appendTail(sb);
ret = sb;
}
return ret.toString();
}
@Override
public void publish(LogRecord record) {
if (record.getLevel().intValue() < getLevel().intValue()) {
return;
}
Level level = record.getLevel();
var query = JSON.object();
query.put("@timestamp", dateTimeFormatter.format(record.getInstant()));
query.put("level", level.getName());
query.put("level_value", level.intValue());
query.put("message", applyReplacements(record.getMessage()));
if (record.getLoggerName() != null) {
query.put("logger_name", record.getLoggerName());
}
var parameters = record.getParameters();
if (parameters != null) {
for (var parameter : parameters) {
if (parameter instanceof Duration duration) {
query.put("duration", duration.toMillis());
}
}
}
if (record.getThrown() != null) {
var writer = new StringWriter();
var printer = new PrintWriter(writer);
record.getThrown().printStackTrace(printer);
query.put("stack_trace", writer.toString());
}
for (var entry : LogContextMap.entrySet()) {
query.put(entry.getKey(), entry.getValue());
}
for (var extraField : getExtraFields(record).entrySet()) {
query.put(extraField.getKey(), extraField.getValue());
}
var httpRequest = HttpRequest.newBuilder()
.uri(endpoint)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(query.toString()))
.build();
var future = httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding());
// Save futures in optional collection when running tests.
if (futures != null) {
futures.add(future);
}
}
void setFuturesCollection(Collection>> futures) {
this.futures = futures;
}
}
================================================
FILE: bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotSlackHandler.java
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.cli;
import org.openjdk.skara.bot.BotTaskAggregationHandler;
import org.openjdk.skara.network.*;
import org.openjdk.skara.json.JSON;
import java.io.IOException;
import java.net.URI;
import java.time.*;
import java.util.*;
import java.util.logging.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
class BotSlackHandler extends BotTaskAggregationHandler {
private final RestRequest webhook;
private final String username;
private final String prefix;
private final Logger log = Logger.getLogger("org.openjdk.skara.bots.cli");;
private final Duration minimumSeparation;
private final Map linkPatterns;
private Instant lastUpdate;
private int dropCount;
BotSlackHandler(URI webhookUrl, String username, String prefix, Duration minimumSeparation, Map links) {
super(true);
webhook = new RestRequest(webhookUrl);
this.username = username;
this.prefix = prefix;
this.minimumSeparation = minimumSeparation;
linkPatterns = links.entrySet().stream()
.collect(Collectors.toMap(entry -> Pattern.compile(entry.getKey(),
Pattern.MULTILINE | Pattern.DOTALL),
Map.Entry::getValue));
lastUpdate = Instant.EPOCH;
dropCount = 0;
}
private Optional getLink(String message) {
for (var linkPattern : linkPatterns.entrySet()) {
var matcher = linkPattern.getKey().matcher(message);
if (matcher.find()) {
return Optional.of(matcher.replaceFirst(linkPattern.getValue()));
}
}
return Optional.empty();
}
private void publishToSlack(String message) {
try {
if (lastUpdate.plus(minimumSeparation).isAfter(Instant.now())) {
dropCount++;
return;
}
if (dropCount > 0) {
message = "_*" + dropCount + "* previous message(s) silently dropped due to throttling_\n" +
message;
}
lastUpdate = Instant.now();
dropCount = 0;
var query = JSON.object();
query.put("text", message);
if (username != null) {
query.put("username", username);
}
var link = getLink(message);
if (link.isPresent()) {
var attachment = JSON.object();
attachment.put("fallback", "Details link");
attachment.put("color", "#cc0e31");
attachment.put("title", "Click for more details");
attachment.put("title_link", link.get());
var attachments = JSON.array();
attachments.add(attachment);
query.put("attachments", attachments);
}
webhook.post("").body(query).executeUnparsed();
} catch (RuntimeException | IOException e) {
log.log(Level.WARNING, "Exception during slack notification posting: " + e.getMessage(), e);
}
}
@Override
public void publishAggregated(List task) {
var message = task.stream()
.map(this::formatMessage)
.collect(Collectors.joining("\n"));
if (!message.isEmpty()) {
publishToSlack(message);
}
}
@Override
public void publishSingle(LogRecord record) {
publishToSlack(formatMessage(record));
}
private String formatMessage(LogRecord record) {
var message = new StringBuilder();
if (prefix != null) {
message.append(prefix);
}
message.append("`").append(record.getLevel().getName()).append("` ").append(record.getMessage());
return message.toString();
}
}
================================================
FILE: bots/cli/src/test/java/org/openjdk/skara/bots/cli/BotLogstashHandlerTests.java
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.cli;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import static org.junit.jupiter.api.Assertions.*;
class BotLogstashHandlerTests {
@Test
void simple() throws IOException, ExecutionException, InterruptedException {
try (var receiver = new RestReceiver()) {
var handler = new BotLogstashHandler(receiver.getEndpoint());
var futures = new ArrayList>>();
handler.setFuturesCollection(futures);
var record = new LogRecord(Level.INFO, "Hello");
record.setLoggerName("my.logger");
handler.publish(record);
for (Future> future : futures) {
future.get();
}
var requests = receiver.getRequests();
assertEquals(1, requests.size(), requests.toString());
assertEquals("Hello", requests.get(0).get("message").asString());
assertEquals(Level.INFO.getName(), requests.get(0).get("level").asString());
assertEquals("my.logger", requests.get(0).get("logger_name").asString());
}
}
@Test
void simpleTask() throws IOException, ExecutionException, InterruptedException {
try (var receiver = new RestReceiver()) {
var handler = new BotLogstashHandler(receiver.getEndpoint());
var futures = new ArrayList>>();
handler.setFuturesCollection(futures);
LoggingBot.runOnce(handler, log -> {
log.warning("Hello");
log.warning("Warning!");
log.warning("Bye");
});
for (Future> future : futures) {
future.get();
}
var requests = receiver.getRequests();
// The async message sending means we may get results in any order. Sort on the
// timestamp to get the actual order.
requests.sort(Comparator.comparing(r -> r.get("@timestamp").toString()));
assertEquals(3, requests.size(), requests.toString());
assertEquals(Level.WARNING.getName(), requests.get(0).get("level").asString());
assertEquals(Level.WARNING.intValue(), requests.get(0).get("level_value").asInt());
assertEquals("Hello", requests.get(0).get("message").asString());
assertEquals("Warning!", requests.get(1).get("message").asString());
assertEquals("Bye", requests.get(2).get("message").asString());
assertEquals(Level.WARNING.toString(), requests.get(0).get("level").asString());
assertNotNull(requests.get(0).get("work_id"), "work_id not set");
assertTrue(requests.get(0).get("work_item").asString().contains("LoggingBot"),
"work_item has bad value " + requests.get(0).get("work_item").asString());
}
}
@Test
void extraField() throws IOException, ExecutionException, InterruptedException {
try (var receiver = new RestReceiver()) {
var handler = new BotLogstashHandler(receiver.getEndpoint());
var futures = new ArrayList>>();
handler.setFuturesCollection(futures);
handler.addExtraField("mandatory", "value");
handler.addExtraField("optional1", "$1", "^H(ello)$");
handler.addExtraField("optional2", "$1", "^(Not found)$");
var record = new LogRecord(Level.INFO, "Hello");
handler.publish(record);
for (Future> future : futures) {
future.get();
}
var requests = receiver.getRequests();
assertEquals(1, requests.size(), requests.toString());
assertEquals("value", requests.get(0).get("mandatory").asString());
assertEquals("ello", requests.get(0).get("optional1").asString());
assertFalse(requests.get(0).contains("optional2"));
}
}
@Test
void extraFieldTask() throws IOException, ExecutionException, InterruptedException {
try (var receiver = new RestReceiver()) {
var handler = new BotLogstashHandler(receiver.getEndpoint());
var futures = new ArrayList>>();
handler.setFuturesCollection(futures);
handler.addExtraField("mandatory", "value");
handler.addExtraField("optional1", "$1", "^H(ello)$");
handler.addExtraField("optional2", "$1", "^(Not found)$");
handler.addExtraField("optional3", "$1", "^B(ye)$");
handler.addExtraField("greedy", "$1", "^(.*)$");
LoggingBot.runOnce(handler, log -> {
log.warning("Hello");
log.warning("Warning!");
log.warning("Bye");
});
for (Future> future : futures) {
future.get();
}
var requests = receiver.getRequests();
// The async message sending means we may get results in any order. Sort on the
// timestamp to get the actual order.
requests.sort(Comparator.comparing(r -> r.get("@timestamp").toString()));
assertEquals(3, requests.size(), requests.toString());
assertEquals("value", requests.get(0).get("mandatory").asString());
assertEquals("ello", requests.get(0).get("optional1").asString());
assertFalse(requests.get(0).contains("optional2"));
assertEquals("ye", requests.get(2).get("optional3").asString());
}
}
}
================================================
FILE: bots/cli/src/test/java/org/openjdk/skara/bots/cli/BotSlackHandlerTests.java
================================================
/*
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.cli;
import org.junit.jupiter.api.*;
import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.logging.*;
import static org.junit.jupiter.api.Assertions.*;
class BotSlackHandlerTests {
@BeforeAll
static void setUp() {
Logger log = Logger.getGlobal();
log.setLevel(Level.FINER);
log = Logger.getLogger("org.openjdk.skara.bot");
log.setLevel(Level.FINER);
}
@Test
void simple() throws IOException {
try (var receiver = new RestReceiver()) {
var handler = new BotSlackHandler(receiver.getEndpoint(), "test", "`testc` ", Duration.ofSeconds(1), new HashMap<>());
var record = new LogRecord(Level.INFO, "Hello");
handler.publish(record);
var requests = receiver.getRequests();
assertEquals(1, requests.size(), requests.toString());
assertEquals("test", requests.get(0).get("username").asString());
assertEquals("`testc` `INFO` Hello", requests.get(0).get("text").asString());
}
}
@Test
void noUser() throws IOException {
try (var receiver = new RestReceiver()) {
var handler = new BotSlackHandler(receiver.getEndpoint(), null, null, Duration.ofSeconds(1), new HashMap<>());
var record = new LogRecord(Level.INFO, "Hello");
handler.publish(record);
var requests = receiver.getRequests();
assertEquals(1, requests.size(), requests.toString());
assertNull(requests.get(0).get("username"));
assertEquals("`INFO` Hello", requests.get(0).get("text").asString());
}
}
@Test
void throttled() throws IOException, InterruptedException {
try (var receiver = new RestReceiver()) {
final var maxDuration = Duration.ofMillis(1500);
var handler = new BotSlackHandler(receiver.getEndpoint(), "test", null, maxDuration, new HashMap<>());
// Post until we hit throttling
var posted = 0;
var maxAttempts = 10000;
var wasThrottled = false;
while (posted < maxAttempts) {
var record = new LogRecord(Level.INFO, "Hello");
handler.publish(record);
posted++;
if (receiver.getRequests().size() != posted) {
wasThrottled = true;
break;
}
}
assertTrue(wasThrottled, "Did not get throttled, is maxDuration too low?");
var requests = receiver.getRequests();
var lastRequest = requests.getLast().asObject();
assertEquals("test", lastRequest.get("username").asString(), lastRequest.toString());
assertTrue(lastRequest.get("text").asString().contains("Hello"), lastRequest.toString());
Thread.sleep(maxDuration);
var record = new LogRecord(Level.INFO, "Hello a final time!");
handler.publish(record);
lastRequest = requests.getLast().asObject();
assertEquals("test", lastRequest.get("username").asString(), lastRequest.toString());
assertTrue(lastRequest.get("text").asString().contains("Hello a final time!"), lastRequest.toString());
assertTrue(lastRequest.get("text").asString().contains("dropped"), lastRequest.toString());
}
}
@Test
void unthrottled() throws IOException, InterruptedException {
try (var receiver = new RestReceiver()) {
var handler = new BotSlackHandler(receiver.getEndpoint(), "test", null, Duration.ofMillis(1), new HashMap<>());
var record = new LogRecord(Level.INFO, "Hello");
handler.publish(record);
Thread.sleep(10);
record = new LogRecord(Level.INFO, "Hello again!");
handler.publish(record);
var requests = receiver.getRequests();
assertEquals(2, requests.size());
assertEquals("test", requests.get(0).get("username").asString());
assertTrue(requests.get(0).get("text").asString().contains("Hello"));
assertEquals("test", requests.get(1).get("username").asString());
assertTrue(requests.get(1).get("text").asString().contains("Hello again!"));
}
}
@Test
void detailsLink() throws IOException {
try (var receiver = new RestReceiver()) {
var details = new HashMap();
details.put(".*error: (xyz).*", "http://go.to/$1");
var handler = new BotSlackHandler(receiver.getEndpoint(), "test", null, Duration.ofMillis(1), details);
var record = new LogRecord(Level.INFO, "Something bad happened. error: xyz occurred");
handler.publish(record);
var requests = receiver.getRequests();
assertEquals(1, requests.size(), requests.toString());
var request = requests.get(0).asObject();
assertEquals("test", request.get("username").asString());
assertTrue(request.get("text").asString().contains("Something bad"));
assertEquals(1, request.get("attachments").asArray().size());
var attachment = request.get("attachments").asArray().get(0);
assertTrue(attachment.get("title_link").asString().contains("go.to/xyz"));
}
}
@Test
void detailsNotMatching() throws IOException {
try (var receiver = new RestReceiver()) {
var details = new HashMap();
details.put(".*error: (xyz).*", "http://go.to/$1");
var handler = new BotSlackHandler(receiver.getEndpoint(), "test", null, Duration.ofMillis(1), details);
var record = new LogRecord(Level.INFO, "Something bad happened. error: abc occurred");
handler.publish(record);
var requests = receiver.getRequests();
assertEquals(1, requests.size(), requests.toString());
var request = requests.get(0).asObject();
assertEquals("test", request.get("username").asString());
assertTrue(request.get("text").asString().contains("Something bad"));
assertFalse(request.contains("attachments"));
}
}
@Test
void taskLog() throws IOException {
try (var receiver = new RestReceiver()) {
var handler = new BotSlackHandler(receiver.getEndpoint(), "test", null, Duration.ZERO, new HashMap<>());
LoggingBot.runOnce(handler, log -> {
log.warning("Hello");
log.warning("Bye");
});
var requests = receiver.getRequests();
assertEquals(1, requests.size(), requests.toString());
assertEquals("test", requests.get(0).get("username").asString());
assertTrue(requests.get(0).get("text").asString().contains("Hello"), requests.get(0).get("text").asString());
assertTrue(requests.get(0).get("text").asString().contains("Bye"), requests.get(0).get("text").asString());
}
}
@Test
void taskLogDetailsLink() throws IOException {
try (var receiver = new RestReceiver()) {
var details = new HashMap();
details.put("error: (def) occured$", "http://go.to/$1");
var handler = new BotSlackHandler(receiver.getEndpoint(), "test", null, Duration.ZERO, details);
LoggingBot.runOnce(handler, log -> {
log.warning("Hello");
log.warning("Something bad happened. error: def occured");
log.warning("Bye");
});
var requests = receiver.getRequests();
assertEquals(1, requests.size(), requests.toString());
var request = requests.get(0).asObject();
assertEquals("test", request.get("username").asString());
assertTrue(request.get("text").asString().contains("Hello"), request.get("text").asString());
assertTrue(request.get("text").asString().contains("Bye"), request.get("text").asString());
assertEquals(1, request.get("attachments").asArray().size());
var attachment = request.get("attachments").asArray().get(0);
assertTrue(attachment.get("title_link").asString().contains("go.to/def"));
}
}
}
================================================
FILE: bots/cli/src/test/java/org/openjdk/skara/bots/cli/LoggingBot.java
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.cli;
import org.openjdk.skara.bot.*;
import org.openjdk.skara.json.JSON;
import java.nio.file.Path;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.logging.*;
public class LoggingBot implements Bot, WorkItem {
private final Consumer runnable;
private final Logger logger;
LoggingBot(Logger logger, Consumer runnable) {
this.runnable = runnable;
this.logger = logger;
}
@Override
public List getPeriodicItems() {
return List.of(this);
}
public static void runOnce(StreamHandler handler, Level handlerLevel, Consumer runnable) {
var log = Logger.getLogger("org.openjdk.skara.bot");
log.setLevel(Level.FINEST);
handler.setLevel(handlerLevel);
log.addHandler(handler);
var bot = new LoggingBot(log, runnable);
try {
var config = JSON.object().put("scratch", JSON.object().put("path", "/tmp"));
var runner = new BotRunner(BotRunnerConfiguration.parse(config), List.of(bot));
runner.runOnce(Duration.ofMinutes(10));
} catch (TimeoutException | ConfigurationError e) {
throw new RuntimeException(e);
}
log.removeHandler(handler);
}
public static void runOnce(StreamHandler handler, Consumer runnable) {
runOnce(handler, Level.WARNING, runnable);
}
@Override
public boolean concurrentWith(WorkItem other) {
return false;
}
@Override
public Collection run(Path scratchPath) {
runnable.accept(logger);
return List.of();
}
@Override
public String name() {
return "logging";
}
@Override
public String botName() {
return name();
}
@Override
public String workItemName() {
return botName();
}
@Override
public String toString() {
return "LoggingBot";
}
}
================================================
FILE: bots/cli/src/test/java/org/openjdk/skara/bots/cli/RestReceiver.java
================================================
/*
* Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.cli;
import com.sun.net.httpserver.*;
import org.openjdk.skara.network.URIBuilder;
import org.openjdk.skara.json.*;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
class RestReceiver implements AutoCloseable {
private final HttpServer server;
private final List requests;
class Handler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
var input = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
requests.add(JSON.parse(input).asObject());
var response = "{}";
exchange.sendResponseHeaders(200, response.length());
OutputStream outputStream = exchange.getResponseBody();
outputStream.write(response.getBytes());
outputStream.close();
}
}
RestReceiver() throws IOException
{
requests = new ArrayList<>();
InetSocketAddress address = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
server = HttpServer.create(address, 0);
server.createContext("/test", new Handler());
server.setExecutor(null);
server.start();
}
URI getEndpoint() {
return URIBuilder.base("http://" + server.getAddress().getHostString() + ":" + server.getAddress().getPort() + "/test").build();
}
List getRequests() {
return requests;
}
@Override
public void close() {
server.stop(0);
}
}
================================================
FILE: bots/common/build.gradle
================================================
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module {
name = 'org.openjdk.skara.bots.common'
test {
requires 'org.junit.jupiter.api'
requires 'org.openjdk.skara.test'
opens 'org.openjdk.skara.bots.common' to 'org.junit.platform.commons'
}
}
dependencies {
implementation project(':vcs')
implementation project(':host')
implementation project(':forge')
implementation project(':issuetracker')
implementation project(':jbs')
implementation project(':json')
implementation project(':network')
implementation project(':jcheck')
implementation project(':census')
testImplementation project(':test')
}
================================================
FILE: bots/common/src/main/java/module-info.java
================================================
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/**
* The bots.common module is meant for application level logic that needs to be
* shared between multiple bots. This is needed for functionality that ties
* together multiple different libraries that we don't want to create
* dependencies between.
*/
module org.openjdk.skara.bots.common {
requires org.openjdk.skara.vcs;
requires transitive org.openjdk.skara.forge;
requires org.openjdk.skara.network;
requires transitive org.openjdk.skara.jbs;
requires transitive org.openjdk.skara.jcheck;
requires java.logging;
exports org.openjdk.skara.bots.common;
}
================================================
FILE: bots/common/src/main/java/org/openjdk/skara/bots/common/BotUtils.java
================================================
/*
* Copyright (c) 2022, 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.common;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* This class contains utility methods used by more than one bot. These methods
* can't reasonably be located in the various libraries as they combine
* functionality and knowledge unique to bot applications. As this class grows,
* it should be encouraged to split it up into more cohesive units.
*/
public class BotUtils {
private static final String lineSep = "(?:\\n|\\r|\\r\\n|\\n\\r)";
private static final Pattern issuesBlockPattern = Pattern.compile(lineSep + lineSep + "###? Issues?((?:" + lineSep + "(?: \\* )?\\[.*)+)", Pattern.MULTILINE);
private static final Pattern issuePattern = Pattern.compile("^(?: \\* )?\\[(\\S+)]\\(.*\\): (.*$)", Pattern.MULTILINE);
public static String escape(String s) {
return s.replace("&", "&").replace("<", "<").replace(">", ">")
.replace("@", "@");
}
/**
* Parses issues from a pull request body and filters out JEP and CSR issues
*
* @param body The Pull Request Body
* @return Set of issue ids
*/
public static Set parseIssues(String body) {
var issuesBlockMatcher = issuesBlockPattern.matcher(body);
if (!issuesBlockMatcher.find()) {
return Set.of();
}
var issueMatcher = issuePattern.matcher(issuesBlockMatcher.group(1));
return issueMatcher.results()
.filter(mr -> !mr.group(2).endsWith(" (**CSR**)") && !mr.group(2).endsWith(" (**CSR**) (Withdrawn)") && !mr.group(2).endsWith(" (**JEP**)"))
.map(mo -> mo.group(1))
.collect(Collectors.toSet());
}
/**
* Parses issues from a pull request body.
*
* @param body The pull request body
* @return Set of issue ids
*/
public static Set parseAllIssues(String body) {
var issuesBlockMatcher = issuesBlockPattern.matcher(body);
if (!issuesBlockMatcher.find()) {
return Set.of();
}
var issueMatcher = issuePattern.matcher(issuesBlockMatcher.group(1));
return issueMatcher.results()
.map(mo -> mo.group(1))
.collect(Collectors.toSet());
}
public static String preprocessCommandLine(String line) {
return line.replaceFirst("/skara\\s+", "/");
}
}
================================================
FILE: bots/common/src/main/java/org/openjdk/skara/bots/common/CommandNameEnum.java
================================================
/*
* Copyright (c) 2023, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.common;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Enum for Skara command names
*/
public enum CommandNameEnum {
help,
integrate,
sponsor,
contributor,
summary(true),
issue,
solves,
reviewers,
csr,
jep,
reviewer,
label,
cc,
clean,
open,
backport,
tag,
branch,
approval(true),
approve,
author,
touch,
keepalive,
template,
trailer;
private boolean isMultiLine = false;
CommandNameEnum() {
}
CommandNameEnum(boolean isMultiLine) {
this.isMultiLine = isMultiLine;
}
public boolean isMultiLine() {
return isMultiLine;
}
/* Utility method for returning command names separated by provided deliminator */
public static String commandNamesSepByDelim(String deliminator) {
return Stream.of(CommandNameEnum.values()).map(CommandNameEnum::name).collect(Collectors.joining(deliminator));
}
}
================================================
FILE: bots/common/src/main/java/org/openjdk/skara/bots/common/PatternEnum.java
================================================
/*
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.common;
import java.util.regex.Pattern;
import static java.util.regex.Pattern.DOTALL;
import static java.util.regex.Pattern.MULTILINE;
import static java.util.regex.Pattern.compile;
/**
* Enum for commonly used Regex patterns
*/
public enum PatternEnum {
EXECUTION_COMMAND_PATTERN(compile("^\\s*/([a-z]+)(?:\\s+|$)(.*)?")),
COMMENT_PATTERN(compile("", DOTALL | MULTILINE));
private final Pattern pattern;
PatternEnum(Pattern pattern) {
this.pattern = pattern;
}
public Pattern getPattern() {
return this.pattern;
}
}
================================================
FILE: bots/common/src/main/java/org/openjdk/skara/bots/common/PullRequestConstants.java
================================================
/*
* Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.common;
import java.util.regex.Pattern;
public class PullRequestConstants {
// MARKERS
public static final String PROGRESS_MARKER = "";
public static final String CSR_NEEDED_MARKER = "";
public static final String CSR_UNNEEDED_MARKER = "";
public static final String JEP_MARKER = ""; //
public static final String WEBREV_COMMENT_MARKER = "";
public static final String TEMPORARY_ISSUE_FAILURE_MARKER = "";
public static final String READY_FOR_SPONSOR_MARKER = "";
public static final String TOUCH_COMMAND_RESPONSE_MARKER = "";
// LABELS
public static final String CSR_LABEL = "csr";
public static final String JEP_LABEL = "jep";
public static final String APPROVAL_LABEL = "approval";
// PATTERNS
public static final Pattern JEP_MARKER_PATTERN = Pattern.compile("");
public static final Pattern READY_FOR_SPONSOR_MARKER_PATTERN = Pattern.compile("");
}
================================================
FILE: bots/common/src/main/java/org/openjdk/skara/bots/common/SolvesTracker.java
================================================
/*
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.common;
import org.openjdk.skara.host.HostUser;
import org.openjdk.skara.issuetracker.Comment;
import org.openjdk.skara.vcs.openjdk.Issue;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.regex.*;
public class SolvesTracker {
private static final String SOLVES_MARKER = "";
private static final Pattern MARKER_PATTERN = Pattern.compile("");
public static String setSolvesMarker(Issue issue) {
var encodedDescription = Base64.getEncoder().encodeToString(issue.description().getBytes(StandardCharsets.UTF_8));
return String.format(SOLVES_MARKER, issue.shortId(), encodedDescription);
}
public static String removeSolvesMarker(Issue issue) {
return String.format(SOLVES_MARKER, issue.shortId(), "");
}
public static List currentSolved(HostUser botUser, List comments, String title) {
var solvesActions = comments.stream()
.filter(comment -> comment.author().equals(botUser))
.flatMap(comment -> comment.body().lines())
.map(MARKER_PATTERN::matcher)
.filter(Matcher::find)
.toList();
var current = new LinkedHashMap();
var titleIssue = Issue.fromStringRelaxed(title);
for (var action : solvesActions) {
var key = action.group(1);
if (titleIssue.isPresent() && key.equals(titleIssue.get().shortId())) {
continue;
}
if (action.group(2).equals("")) {
current.remove(key);
} else {
var decodedDescription = new String(Base64.getDecoder().decode(action.group(2)), StandardCharsets.UTF_8);
var issue = new Issue(key, decodedDescription);
current.put(key, issue);
}
}
return new ArrayList<>(current.values());
}
public static Optional getLatestSolvesActionComment(HostUser botUser, List comments, Issue issue) {
return comments.stream()
.filter(comment -> comment.author().equals(botUser))
.filter(comment -> comment.body().contains("";
for (var pr : prs) {
if (pr.title().equals(title) &&
pr.targetRef().equals(toBranch.name()) &&
pr.body().startsWith(marker) &&
currentUser.equals(pr.author())) {
// Yes, this could be optimized do a merge "this turn", but it is much simpler
// to just wait until the next time the bot runs
shouldMerge = false;
}
}
// Check if merge should happen at this time
if (spec.frequency().isPresent()) {
var now = clock.now();
var desc = toBranch.name() + "->" + fromRepo.name() + ":" + fromBranch.name();
var freq = spec.frequency().get();
if (freq.isHourly()) {
if (!hourly.containsKey(desc)) {
hourly.put(desc, new HashSet<>());
}
var minute = now.getMinute();
var hour = now.getHour();
if (freq.minute() == minute && !hourly.get(desc).contains(hour)) {
hourly.get(desc).add(hour);
} else {
shouldMerge = false;
}
} else if (freq.isDaily()) {
if (!daily.containsKey(desc)) {
daily.put(desc, new HashSet<>());
}
var hour = now.getHour();
var day = now.getDayOfYear();
if (freq.hour() == hour && !daily.get(desc).contains(day)) {
daily.get(desc).add(day);
} else {
shouldMerge = false;
}
} else if (freq.isWeekly()) {
if (!weekly.containsKey(desc)) {
weekly.put(desc, new HashSet<>());
}
var weekOfYear = now.get(WeekFields.ISO.weekOfYear());
var weekday = now.getDayOfWeek();
var hour = now.getHour();
if (freq.weekday().equals(weekday) &&
freq.hour() == hour &&
!weekly.get(desc).contains(weekOfYear)) {
weekly.get(desc).add(weekOfYear);
} else {
shouldMerge = false;
}
} else if (freq.isMonthly()) {
if (!monthly.containsKey(desc)) {
monthly.put(desc, new HashSet<>());
}
var day = now.getDayOfMonth();
var hour = now.getHour();
var month = now.getMonth();
if (freq.day() == day && freq.hour() == hour &&
!monthly.get(desc).contains(month)) {
monthly.get(desc).add(month);
} else {
shouldMerge = false;
}
} else if (freq.isYearly()) {
if (!yearly.containsKey(desc)) {
yearly.put(desc, new HashSet<>());
}
var month = now.getMonth();
var day = now.getDayOfMonth();
var hour = now.getHour();
var year = now.getYear();
if (freq.month().equals(month) &&
freq.day() == day &&
freq.hour() == hour &&
!yearly.get(desc).contains(year)) {
yearly.get(desc).add(year);
} else {
shouldMerge = false;
}
}
}
// Check if any prerequisite repository has a conflict pull request open
if (shouldMerge) {
for (var prereq : spec.prerequisites()) {
var openMergeConflictPRs = prereq.openPullRequests()
.stream()
.filter(pr -> pr.title().startsWith("Merge "))
.filter(pr -> pr.body().startsWith(marker))
.map(PullRequest::id)
.collect(Collectors.toList());
if (!openMergeConflictPRs.isEmpty()) {
log.info("Will not merge because the prerequisite " + prereq.name() +
" has open merge conflicts PRs: " +
String.join(", ", openMergeConflictPRs));
shouldMerge = false;
}
}
}
// Check if any dependencies failed
if (shouldMerge) {
if (spec.dependencies().stream().anyMatch(unmerged::contains)) {
var failed = spec.dependencies()
.stream()
.filter(unmerged::contains)
.collect(Collectors.toList());
log.info("Will not merge because the following dependencies did not merge successfully: " +
String.join(", ", failed));
shouldMerge = false;
}
}
if (!shouldMerge) {
log.info("Will not merge " + fromRepo.name() + ":" + fromBranch.name() + " to " + toBranch.name());
if (spec.name().isPresent()) {
unmerged.add(spec.name().get());
}
continue;
}
// Checkout the branch to merge into
repo.checkout(toBranch, false);
var remoteBranch = new Branch(repo.upstreamFor(toBranch).orElseThrow(() ->
new IllegalStateException("Could not get remote branch name for " + toBranch.name())
));
repo.merge(remoteBranch, Repository.FastForward.ONLY);
if (!repo.isClean()) {
throw new RuntimeException("Local repository isn't clean after fast-forward merge - has the fork diverged unexpectedly?");
}
log.info("Trying to merge " + fromRepo.name() + ":" + fromBranch.name() + " to " + toBranch.name());
log.info("Fetching " + fromRepo.name() + ":" + fromBranch.name());
var fetchHead = repo.fetch(fromRepo.authenticatedUrl(), fromBranch.name(), false).orElseThrow();
var head = repo.resolve(toBranch.name()).orElseThrow(() ->
new IOException("Could not resolve branch " + toBranch.name())
);
if (repo.contains(toBranch, fetchHead)) {
log.info("Nothing to merge");
continue;
}
var isAncestor = repo.isAncestor(head, fetchHead);
log.info("Merging into " + toBranch.name());
IOException error = null;
try {
repo.merge(fetchHead);
} catch (IOException e) {
error = e;
}
if (error == null) {
log.info("Pushing successful merge");
if (!isAncestor) {
repo.commit("Automatic merge of " + fromDesc + " into " + toBranch,
"duke", "duke@openjdk.org");
}
try {
repo.push(toBranch, target.authenticatedUrl().toString(), false);
} catch (IOException e) {
// A failed push can result in the local and remote branch diverging,
// re-create the repository from the remote.
// No need to create a pull request, just retry the merge and the push
// the next run.
deleteDirectory(dir);
repo = cloneAndSyncFork(dir);
}
} else {
if (spec.name().isPresent()) {
unmerged.add(spec.name().get());
}
log.info("Got error: " + error.getMessage());
log.info("Aborting unsuccesful merge");
var status = repo.status();
repo.abortMerge();
var numBranchesInFork = repo.remoteBranches(fork.authenticatedUrl().toString()).size();
var branchDesc = Integer.toString(numBranchesInFork + 1);
repo.push(fetchHead, fork.authenticatedUrl(), branchDesc);
log.info("Creating pull request to alert");
var mergeBase = repo.mergeBase(fetchHead, head);
var message = new ArrayList();
message.add(marker);
message.add("");
var commits = repo.commitMetadata(mergeBase.hex() + ".." + fetchHead.hex(), true);
var numCommits = commits.size();
var are = numCommits > 1 ? "are" : "is";
var s = numCommits > 1 ? "s" : "";
message.add("Hi all,");
message.add("");
message.add("this is an _automatically_ generated pull request to notify you that there " +
are + " " + numCommits + " commit" + s + " from the branch `" + fromDesc + "`" +
"that can **not** be merged into the branch `" + toBranch.name() + "`:");
message.add("");
var unmergedFiles = status.stream().filter(entry -> entry.status().isUnmerged()).collect(Collectors.toList());
if (unmergedFiles.size() <= 10) {
var files = unmergedFiles.size() > 1 ? "files" : "file";
message.add("The following " + files + " contains merge conflicts:");
message.add("");
for (var fileStatus : unmergedFiles) {
message.add("- " + fileStatus.source().path().orElseThrow());
}
} else {
message.add("Over " + unmergedFiles.size() + " files contains merge conflicts.");
}
message.add("");
var project = JCheckConfiguration.from(repo, head).map(conf -> conf.general().project());
if (project.isPresent()) {
message.add("All Committers in this [project](https://openjdk.org/census#" + project.get() + ") " +
"have access to my [personal fork](" + fork.nonTransformedWebUrl() + ") and can " +
"therefore help resolve these merge conflicts (you may want to coordinate " +
"who should do this).");
} else {
message.add("All users with access to my [personal fork](" + fork.nonTransformedWebUrl() + ") " +
"can help resolve these merge conflicts " +
"(you may want to coordinate who should do this).");
}
message.add("The following paragraphs will give an example on how to solve these " +
"merge conflicts and push the resulting merge commit to this pull request.");
message.add("The below commands should be run in a local clone of your " +
"[personal fork](https://wiki.openjdk.org/display/skara#Skara-Personalforks) " +
"of the [" + target.name() + "](" + target.nonTransformedWebUrl() + ") repository.");
message.add("");
var localBranchName = "openjdk-bot-" + branchDesc;
message.add("```bash");
message.add("# Ensure target branch is up to date");
message.add("$ git checkout " + toBranch.name());
message.add("$ git pull " + target.nonTransformedWebUrl() + ".git " + toBranch.name());
message.add("");
message.add("# Fetch and checkout the branch for this pull request");
message.add("$ git fetch " + fork.nonTransformedWebUrl() + ".git +" + branchDesc + ":" + localBranchName);
message.add("$ git checkout " + localBranchName);
message.add("");
message.add("# Merge the target branch");
message.add("$ git merge " + toBranch.name());
message.add("```");
message.add("");
message.add("When you have resolved the conflicts resulting from the `git merge` command " +
"above, run the following commands to create a merge commit:");
message.add("");
message.add("```bash");
message.add("$ git add paths/to/files/with/conflicts");
message.add("$ git commit -m 'Merge " + fromDesc + "'");
message.add("```");
message.add("");
message.add("");
message.add("When you have created the merge commit, run the following command to push the merge commit " +
"to this pull request:");
message.add("");
message.add("```bash");
message.add("$ git push " + fork.nonTransformedWebUrl() + ".git " + localBranchName + ":" + branchDesc);
message.add("```");
message.add("");
message.add("_Note_: if you are using SSH to push commits to GitHub, then change the URL in the above `git push` command accordingly.");
message.add("");
message.add("Thanks,");
message.add("J. Duke");
message.add("");
message.add("/integrate auto");
var pr = fork.createPullRequest(prTarget,
toBranch.name(),
branchDesc,
title,
message);
pr.addLabel("failed-auto-merge");
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return List.of();
}
@Override
public String toString() {
return "MergeBot@(" + target.name() + ")";
}
@Override
public List getPeriodicItems() {
return List.of(this);
}
@Override
public String name() {
return MergeBotFactory.NAME;
}
@Override
public String botName() {
return name();
}
@Override
public String workItemName() {
return botName();
}
public List getSpecs() {
return specs;
}
}
================================================
FILE: bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBotFactory.java
================================================
/*
* Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.merge;
import org.openjdk.skara.bot.*;
import org.openjdk.skara.json.JSON;
import org.openjdk.skara.vcs.Branch;
import java.io.*;
import java.nio.file.Files;
import java.time.DayOfWeek;
import java.time.Month;
import java.util.*;
import java.util.stream.Collectors;
import java.util.logging.Logger;
public class MergeBotFactory implements BotFactory {
private final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
static final String NAME = "merge";
@Override
public String name() {
return NAME;
}
private static MergeBot.Spec.Frequency.Interval toInterval(String s) {
switch (s.toLowerCase()) {
case "hourly":
return MergeBot.Spec.Frequency.Interval.HOURLY;
case "daily":
return MergeBot.Spec.Frequency.Interval.DAILY;
case "weekly":
return MergeBot.Spec.Frequency.Interval.WEEKLY;
case "monthly":
return MergeBot.Spec.Frequency.Interval.MONTHLY;
case "yearly":
return MergeBot.Spec.Frequency.Interval.YEARLY;
default:
throw new IllegalArgumentException("Unknown interval: " + s);
}
}
private static DayOfWeek toWeekday(String s) {
switch (s.toLowerCase()) {
case "monday":
return DayOfWeek.MONDAY;
case "tuesday":
return DayOfWeek.TUESDAY;
case "wednesday":
return DayOfWeek.WEDNESDAY;
case "thursday":
return DayOfWeek.THURSDAY;
case "friday":
return DayOfWeek.FRIDAY;
case "saturday":
return DayOfWeek.SATURDAY;
case "sunday":
return DayOfWeek.SUNDAY;
default:
throw new IllegalArgumentException("Unknown weekday: " + s);
}
}
private static Month toMonth(String s) {
switch (s.toLowerCase()) {
case "january":
return Month.JANUARY;
case "february":
return Month.FEBRUARY;
case "march":
return Month.MARCH;
case "april":
return Month.APRIL;
case "may":
return Month.MAY;
case "june":
return Month.JUNE;
case "july":
return Month.JULY;
case "august":
return Month.AUGUST;
case "september":
return Month.SEPTEMBER;
case "october":
return Month.OCTOBER;
case "november":
return Month.NOVEMBER;
case "december":
return Month.DECEMBER;
default:
throw new IllegalArgumentException("Unknown month: " + s);
}
}
private static int toDay(int i) {
if (i < 0 || i > 30) {
throw new IllegalArgumentException("Unknown day: " + i);
}
return i;
}
private static int toHour(int i) {
if (i < 0 || i > 23) {
throw new IllegalArgumentException("Unknown hour: " + i);
}
return i;
}
private static int toMinute(int i) {
if (i < 0 || i > 59) {
throw new IllegalArgumentException("Unknown minute: " + i);
}
return i;
}
@Override
public List create(BotConfiguration configuration) {
var storage = configuration.storageFolder();
try {
Files.createDirectories(storage);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
var specific = configuration.specific();
var bots = new ArrayList();
for (var repo : specific.get("repositories").asArray()) {
var targetRepo = configuration.repository(repo.get("target").asString());
var forkRepo = configuration.repository(repo.get("fork").asString());
var specs = new ArrayList();
for (var spec : repo.get("spec").asArray()) {
var from = spec.get("from").asString().split(":");
var fromRepo = configuration.repository(from[0]);
var fromBranch = new Branch(from[1]);
var toBranch = new Branch(spec.get("to").asString());
MergeBot.Spec.Frequency frequency = null;
if (spec.contains("frequency")) {
var freq = spec.get("frequency").asObject();
var interval = toInterval(freq.get("interval").asString());
if (interval.isHourly()) {
var minute = toMinute(freq.get("minute").asInt());
frequency = MergeBot.Spec.Frequency.hourly(minute);
} else if (interval.isDaily()) {
var hour = toHour(freq.get("hour").asInt());
frequency = MergeBot.Spec.Frequency.daily(hour);
} else if (interval.isWeekly()) {
var weekday = toWeekday(freq.get("weekday").asString());
var hour = toHour(freq.get("hour").asInt());
frequency = MergeBot.Spec.Frequency.weekly(weekday, hour);
} else if (interval.isMonthly()) {
var day = toDay(freq.get("day").asInt());
var hour = toHour(freq.get("hour").asInt());
frequency = MergeBot.Spec.Frequency.monthly(day, hour);
} else if (interval.isYearly()) {
var month = toMonth(freq.get("month").asString());
var day = toDay(freq.get("day").asInt());
var hour = toHour(freq.get("hour").asInt());
frequency = MergeBot.Spec.Frequency.yearly(month, day, hour);
} else {
throw new IllegalStateException("Unexpected interval: " + interval);
}
}
var name = spec.getOrDefault("name", JSON.of()).asString();
var dependencies = spec.getOrDefault("dependencies", JSON.array())
.stream()
.map(e -> e.asString())
.collect(Collectors.toList());
var prerequisites = spec.getOrDefault("prerequisites", JSON.array())
.stream()
.map(e -> e.asString())
.map(configuration::repository)
.collect(Collectors.toList());
specs.add(new MergeBot.Spec(fromRepo,
fromBranch,
toBranch,
frequency,
name,
dependencies,
prerequisites));
}
bots.add(new MergeBot(storage, targetRepo, forkRepo, specs));
}
return bots;
}
}
================================================
FILE: bots/merge/src/test/java/org/openjdk/skara/bots/merge/MergeBotFactoryTest.java
================================================
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.merge;
import org.junit.jupiter.api.Test;
import org.openjdk.skara.json.JWCC;
import org.openjdk.skara.test.TemporaryDirectory;
import org.openjdk.skara.test.TestBotFactory;
import org.openjdk.skara.test.TestHostedRepository;
import java.time.DayOfWeek;
import java.time.Month;
import static org.junit.jupiter.api.Assertions.*;
class MergeBotFactoryTest {
@Test
public void testCreate() {
try (var tempFolder = new TemporaryDirectory()) {
String jsonString = """
{
"repositories": [
{
"target": "target",
"fork": "fork",
"spec": [
{
"from": "from1:master",
"to": "master",
"frequency": {
"interval": "weekly",
"weekday": "monday",
"hour": 3
}
},
{
"name": "spec2",
"from": "from2:master",
"to": "test"
},
{
"from": "from3:master",
"to": "master",
"frequency": {
"interval": "hourly",
"minute": 30
}
},
{
"from": "from4:master",
"to": "master",
"frequency": {
"interval": "daily",
"hour": 2
}
},
{
"from": "from5:master",
"to": "master",
"frequency": {
"interval": "monthly",
"day": 1,
"hour": 2
}
},
{
"from": "from6:master",
"to": "master",
"frequency": {
"interval": "yearly",
"month": "october",
"day": 15,
"hour": 5
}
}
]
}
]
}
""";
var jsonConfig = JWCC.parse(jsonString).asObject();
var testBotFactory = TestBotFactory.newBuilder()
.addHostedRepository("target", new TestHostedRepository("target"))
.addHostedRepository("fork", new TestHostedRepository("fork"))
.addHostedRepository("from1", new TestHostedRepository("from1"))
.addHostedRepository("from2", new TestHostedRepository("from2"))
.addHostedRepository("from3", new TestHostedRepository("from3"))
.addHostedRepository("from4", new TestHostedRepository("from4"))
.addHostedRepository("from5", new TestHostedRepository("from5"))
.addHostedRepository("from6", new TestHostedRepository("from6"))
.storagePath(tempFolder.path().resolve("storage"))
.build();
var bots = testBotFactory.createBots(MergeBotFactory.NAME, jsonConfig);
assertEquals(1, bots.size());
MergeBot mergeBot = (MergeBot) bots.get(0);
assertEquals("MergeBot@(target)", mergeBot.toString());
// Check the contents in the mergeBot
var specs = mergeBot.getSpecs();
MergeBot.Spec spec1 = specs.get(0);
MergeBot.Spec.Frequency frequency1 = spec1.frequency().get();
assertTrue(spec1.name().isEmpty());
assertTrue(frequency1.isWeekly());
assertEquals(DayOfWeek.MONDAY, frequency1.weekday());
assertEquals(3, frequency1.hour());
MergeBot.Spec spec2 = specs.get(1);
assertTrue(spec2.frequency().isEmpty());
assertTrue(spec2.name().isPresent());
MergeBot.Spec spec3 = specs.get(2);
MergeBot.Spec.Frequency frequency3 = spec3.frequency().get();
assertTrue(frequency3.isHourly());
assertEquals(30, frequency3.minute());
MergeBot.Spec spec4 = specs.get(3);
MergeBot.Spec.Frequency frequency4 = spec4.frequency().get();
assertTrue(frequency4.isDaily());
assertEquals(2, frequency4.hour());
MergeBot.Spec spec5 = specs.get(4);
MergeBot.Spec.Frequency frequency5 = spec5.frequency().get();
assertTrue(frequency5.isMonthly());
assertEquals(1, frequency5.day());
assertEquals(2, frequency5.hour());
MergeBot.Spec spec6 = specs.get(5);
MergeBot.Spec.Frequency frequency6 = spec6.frequency().get();
assertTrue(frequency6.isYearly());
assertEquals(Month.OCTOBER, frequency6.month());
assertEquals(15, frequency6.day());
assertEquals(5, frequency6.hour());
}
}
}
================================================
FILE: bots/merge/src/test/java/org/openjdk/skara/bots/merge/MergeBotTests.java
================================================
/*
* Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.merge;
import org.junit.jupiter.api.*;
import org.openjdk.skara.host.HostUser;
import org.openjdk.skara.issuetracker.Issue;
import org.openjdk.skara.test.*;
import org.openjdk.skara.vcs.*;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.*;
import java.util.*;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.*;
class MergeBotTests {
@Test
void mergeMasterBranch(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var toGitConfig = toDir.resolve(".git").resolve("config");
Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var forkDir = temp.path().resolve("fork.git");
var forkLocalRepo = TestableRepository.init(forkDir, VCS.GIT);
var forkGitConfig = forkDir.resolve(".git").resolve("config");
Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo);
var now = ZonedDateTime.now();
var fromFileA = fromDir.resolve("a.txt");
Files.writeString(fromFileA, "Hello A\n");
fromLocalRepo.add(fromFileA);
var fromHashA = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(fromHashA, fromCommits.get(0).hash());
var toFileA = toDir.resolve("a.txt");
Files.writeString(toFileA, "Hello A\n");
toLocalRepo.add(toFileA);
var toHashA = toLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(toHashA, toCommits.get(0).hash());
assertEquals(fromHashA, toHashA);
var fromFileB = fromDir.resolve("b.txt");
Files.writeString(fromFileB, "Hello B\n");
fromLocalRepo.add(fromFileB);
var fromHashB = fromLocalRepo.commit("Adding b.txt", "duke", "duke@openjdk.org");
var toFileC = toDir.resolve("c.txt");
Files.writeString(toFileC, "Hello C\n");
toLocalRepo.add(toFileC);
var toHashC = toLocalRepo.commit("Adding c.txt", "duke", "duke@openjdk.org");
var storage = temp.path().resolve("storage");
var master = new Branch("master");
var specs = List.of(new MergeBot.Spec(fromHostedRepo, master, master));
var bot = new MergeBot(storage, toHostedRepo, toFork, specs);
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
var hashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
assertTrue(hashes.contains(toHashA));
assertTrue(hashes.contains(fromHashB));
assertTrue(hashes.contains(toHashC));
var known = Set.of(toHashA, fromHashB, toHashC);
var merge = toCommits.stream().filter(c -> !known.contains(c.hash())).findAny().get();
assertTrue(merge.isMerge());
assertEquals(List.of("Automatic merge of test:master into master"), merge.message());
assertEquals("duke", merge.author().name());
assertEquals("duke@openjdk.org", merge.author().email());
assertEquals(0, toHostedRepo.openPullRequests().size());
}
}
@Test
void successfulDependency(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory(false)) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var toGitConfig = toDir.resolve(".git").resolve("config");
Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var forkDir = temp.path().resolve("fork.git");
var forkLocalRepo = TestableRepository.init(forkDir, VCS.GIT);
var forkGitConfig = forkDir.resolve(".git").resolve("config");
Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo);
var now = ZonedDateTime.now();
var fromFileA = fromDir.resolve("a.txt");
Files.writeString(fromFileA, "Hello A\n");
fromLocalRepo.add(fromFileA);
var fromHashA = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(fromHashA, fromCommits.get(0).hash());
var toFileA = toDir.resolve("a.txt");
Files.writeString(toFileA, "Hello A\n");
toLocalRepo.add(toFileA);
var toHashA = toLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(toHashA, toCommits.get(0).hash());
assertEquals(fromHashA, toHashA);
toLocalRepo.branch(toHashA, "feature");
assertEquals(2, toLocalRepo.branches().size());
var fromFileB = fromDir.resolve("b.txt");
Files.writeString(fromFileB, "Hello B\n");
fromLocalRepo.add(fromFileB);
var fromHashB = fromLocalRepo.commit("Adding b.txt", "duke", "duke@openjdk.org");
var featureBranch = fromLocalRepo.branch(fromHashB, "feature");
fromLocalRepo.checkout(featureBranch);
var fromFileD = fromDir.resolve("d.txt");
Files.writeString(fromFileD, "Hello D\n");
fromLocalRepo.add(fromFileD);
var fromHashD = fromLocalRepo.commit("Adding d.txt", "duke", "duke@openjdk.org");
var toFileC = toDir.resolve("c.txt");
Files.writeString(toFileC, "Hello C\n");
toLocalRepo.add(toFileC);
var toHashC = toLocalRepo.commit("Adding c.txt", "duke", "duke@openjdk.org");
toLocalRepo.checkout(featureBranch);
var toFileE = toDir.resolve("e.txt");
Files.writeString(toFileE, "Hello E\n");
toLocalRepo.add(toFileE);
var toHashE = toLocalRepo.commit("Adding e.txt", "duke", "duke@openjdk.org");
var storage = temp.path().resolve("storage");
var master = new Branch("master");
var feature = new Branch("feature");
var specs = List.of(new MergeBot.Spec(fromHostedRepo, master, master, null, "master", List.of(), List.of()),
new MergeBot.Spec(fromHostedRepo, feature, feature, null, "feature", List.of("master"), List.of()));
var bot = new MergeBot(storage, toHostedRepo, toFork, specs);
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(7, toCommits.size());
var hashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
assertTrue(hashes.contains(toHashA));
assertTrue(hashes.contains(fromHashB));
assertTrue(hashes.contains(toHashC));
var merges = toCommits.stream().filter(c -> c.isMerge()).collect(Collectors.toList());
assertEquals(2, merges.size());
assertTrue(merges.stream().anyMatch(c -> c.message().get(0).equals("Automatic merge of test:master into master")));
assertTrue(merges.stream().anyMatch(c -> c.message().get(0).equals("Automatic merge of test:feature into feature")));
}
}
@Test
void failedDependency(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory(false)) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var toGitConfig = toDir.resolve(".git").resolve("config");
Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var forkDir = temp.path().resolve("fork.git");
var forkLocalRepo = TestableRepository.init(forkDir, VCS.GIT);
var forkGitConfig = forkDir.resolve(".git").resolve("config");
Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo);
var now = ZonedDateTime.now();
var fromFileA = fromDir.resolve("a.txt");
Files.writeString(fromFileA, "Hello A\n");
fromLocalRepo.add(fromFileA);
var fromHashA = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(fromHashA, fromCommits.get(0).hash());
var toFileA = toDir.resolve("a.txt");
Files.writeString(toFileA, "Hello A\n");
toLocalRepo.add(toFileA);
var toHashA = toLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(toHashA, toCommits.get(0).hash());
assertEquals(fromHashA, toHashA);
toLocalRepo.branch(toHashA, "feature");
assertEquals(2, toLocalRepo.branches().size());
var fromFileB = fromDir.resolve("b.txt");
Files.writeString(fromFileB, "Hello B\n");
fromLocalRepo.add(fromFileB);
var fromHashB = fromLocalRepo.commit("Adding b.txt", "duke", "duke@openjdk.org");
var featureBranch = fromLocalRepo.branch(fromHashB, "feature");
fromLocalRepo.checkout(featureBranch);
var fromFileD = fromDir.resolve("d.txt");
Files.writeString(fromFileD, "Hello D\n");
fromLocalRepo.add(fromFileD);
var fromHashD = fromLocalRepo.commit("Adding d.txt", "duke", "duke@openjdk.org");
var toFileB = toDir.resolve("b.txt");
Files.writeString(toFileB, "Hello conflict\n");
toLocalRepo.add(toFileB);
var toHashB = toLocalRepo.commit("Adding b.txt", "duke", "duke@openjdk.org");
toLocalRepo.checkout(featureBranch);
var toFileE = toDir.resolve("e.txt");
Files.writeString(toFileE, "Hello E\n");
toLocalRepo.add(toFileE);
var toHashE = toLocalRepo.commit("Adding e.txt", "duke", "duke@openjdk.org");
var toCommitsBeforeMerge = toLocalRepo.commits().asList();
assertEquals(3, toCommitsBeforeMerge.size());
assertEquals(toHashE, toCommitsBeforeMerge.get(0).hash());
assertEquals(toHashB, toCommitsBeforeMerge.get(1).hash());
assertEquals(toHashA, toCommitsBeforeMerge.get(2).hash());
assertEquals(toHashB, toLocalRepo.resolve("master").get());
assertEquals(toHashE, toLocalRepo.resolve("feature").get());
var storage = temp.path().resolve("storage");
var master = new Branch("master");
var feature = new Branch("feature");
var specs = List.of(new MergeBot.Spec(fromHostedRepo, master, master, null, "master", List.of(), List.of()),
new MergeBot.Spec(fromHostedRepo, feature, feature, null, "feature", List.of("master"), List.of()));
var bot = new MergeBot(storage, toHostedRepo, toFork, specs);
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(toCommitsBeforeMerge.size(), toCommits.size());
assertEquals(toCommitsBeforeMerge.get(0).hash(), toCommits.get(0).hash());
assertEquals(toCommitsBeforeMerge.get(1).hash(), toCommits.get(1).hash());
assertEquals(toCommitsBeforeMerge.get(2).hash(), toCommits.get(2).hash());
assertEquals(toHashB, toLocalRepo.resolve("master").get());
assertEquals(toHashE, toLocalRepo.resolve("feature").get());
}
}
@Test
void failingMergeTest(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var toGitConfig = toDir.resolve(".git").resolve("config");
Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var forkDir = temp.path().resolve("fork.git");
var forkLocalRepo = TestableRepository.init(forkDir, VCS.GIT);
var forkGitConfig = forkDir.resolve(".git").resolve("config");
Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo);
var now = ZonedDateTime.now();
var fromFileA = fromDir.resolve("a.txt");
Files.writeString(fromFileA, "Hello A\n");
fromLocalRepo.add(fromFileA);
var fromHashA = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(fromHashA, fromCommits.get(0).hash());
var toFileA = toDir.resolve("a.txt");
Files.writeString(toFileA, "Hello A\n");
toLocalRepo.add(toFileA);
var toHashA = toLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(toHashA, toCommits.get(0).hash());
assertEquals(fromHashA, toHashA);
var fromFileB = fromDir.resolve("b.txt");
Files.writeString(fromFileB, "Hello B1\n");
fromLocalRepo.add(fromFileB);
var fromHashB = fromLocalRepo.commit("Adding b1.txt", "duke", "duke@openjdk.org");
var toFileB = toDir.resolve("b.txt");
Files.writeString(toFileB, "Hello B2\n");
toLocalRepo.add(toFileB);
var toHashB = toLocalRepo.commit("Adding b2.txt", "duke", "duke@openjdk.org");
var storage = temp.path().resolve("storage");
var master = new Branch("master");
var specs = List.of(new MergeBot.Spec(fromHostedRepo, master, master));
var bot = new MergeBot(storage, toHostedRepo, toFork, specs);
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(2, toCommits.size());
var toHashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
assertTrue(toHashes.contains(toHashA));
assertTrue(toHashes.contains(toHashB));
var pullRequests = toHostedRepo.openPullRequests();
assertEquals(1, pullRequests.size());
var pr = pullRequests.get(0);
assertEquals("Merge test:master", pr.title());
assertTrue(pr.labelNames().contains("failed-auto-merge"));
}
}
@Test
void failingPrerequisite(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var toGitConfig = toDir.resolve(".git").resolve("config");
Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var forkDir = temp.path().resolve("fork.git");
var forkLocalRepo = TestableRepository.init(forkDir, VCS.GIT);
var forkGitConfig = forkDir.resolve(".git").resolve("config");
Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo);
var now = ZonedDateTime.now();
var fromFileA = fromDir.resolve("a.txt");
Files.writeString(fromFileA, "Hello A\n");
fromLocalRepo.add(fromFileA);
var fromHashA = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(fromHashA, fromCommits.get(0).hash());
var toFileA = toDir.resolve("a.txt");
Files.writeString(toFileA, "Hello A\n");
toLocalRepo.add(toFileA);
var toHashA = toLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(toHashA, toCommits.get(0).hash());
assertEquals(fromHashA, toHashA);
var fromFileB = fromDir.resolve("b.txt");
Files.writeString(fromFileB, "Hello B1\n");
fromLocalRepo.add(fromFileB);
var fromHashB = fromLocalRepo.commit("Adding b1.txt", "duke", "duke@openjdk.org");
var toFileB = toDir.resolve("b.txt");
Files.writeString(toFileB, "Hello B2\n");
toLocalRepo.add(toFileB);
var toHashB = toLocalRepo.commit("Adding b2.txt", "duke", "duke@openjdk.org");
var storage = temp.path().resolve("storage");
var master = new Branch("master");
var specs = List.of(new MergeBot.Spec(fromHostedRepo, master, master));
var bot = new MergeBot(storage, toHostedRepo, toFork, specs);
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(2, toCommits.size());
var toHashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
assertTrue(toHashes.contains(toHashA));
assertTrue(toHashes.contains(toHashB));
var pullRequests = toHostedRepo.openPullRequests();
assertEquals(1, pullRequests.size());
var pr = pullRequests.get(0);
assertEquals("Merge test:master", pr.title());
var fromDir2 = temp.path().resolve("from2.git");
var fromLocalRepo2 = TestableRepository.init(fromDir2, VCS.GIT);
var fromHostedRepo2 = new TestHostedRepository(host, "test-2", fromLocalRepo2);
var host2 = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var toDir2 = temp.path().resolve("to2.git");
var toLocalRepo2 = TestableRepository.init(toDir2, VCS.GIT);
var toGitConfig2 = toDir2.resolve(".git").resolve("config");
Files.write(toGitConfig2, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo2 = new TestHostedRepository(host2, "test-mirror-2", toLocalRepo2);
var forkDir2 = temp.path().resolve("fork2.git");
var forkLocalRepo2 = TestableRepository.init(forkDir2, VCS.GIT);
var forkGitConfig2 = forkDir2.resolve(".git").resolve("config");
Files.write(forkGitConfig2, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork2 = new TestHostedRepository(host2, "test-mirror-fork-2", forkLocalRepo2);
var now2 = ZonedDateTime.now();
var fromFileA2 = fromDir2.resolve("a2.txt");
Files.writeString(fromFileA2, "Hello A2\n");
fromLocalRepo2.add(fromFileA2);
var fromHashA2 = fromLocalRepo2.commit("Adding a2.txt", "duke", "duke@openjdk.org", now2);
var toFileA2 = toDir2.resolve("a2.txt");
Files.writeString(toFileA2, "Hello A2\n");
toLocalRepo2.add(toFileA2);
var toHashA2 = toLocalRepo2.commit("Adding a2.txt", "duke", "duke@openjdk.org", now2);
var toCommits2 = toLocalRepo2.commits().asList();
assertEquals(1, toCommits2.size());
assertEquals(toHashA2, toCommits2.get(0).hash());
assertEquals(fromHashA2, toHashA2);
var fromFileB2 = fromDir2.resolve("b2.txt");
Files.writeString(fromFileB2, "Hello B2\n");
fromLocalRepo2.add(fromFileB2);
var fromHashB2 = fromLocalRepo2.commit("Adding b2.txt", "duke", "duke@openjdk.org");
var fromCommits2 = fromLocalRepo2.commits().asList();
assertEquals(2, fromCommits2.size());
assertEquals(fromHashB2, fromCommits2.get(0).hash());
assertEquals(fromHashA2, fromCommits2.get(1).hash());
var storage2 = temp.path().resolve("storage-2");
var master2 = new Branch("master");
var specs2 = List.of(new MergeBot.Spec(fromHostedRepo2, master2, master2, null, "master", List.of(), List.of(toHostedRepo)));
var bot2 = new MergeBot(storage2, toHostedRepo2, toFork2, specs2);
TestBotRunner.runPeriodicItems(bot2);
var toCommitsAfterMerge2 = toLocalRepo2.commits().asList();
assertEquals(1, toCommitsAfterMerge2.size());
assertEquals(toHashA2, toCommitsAfterMerge2.get(0).hash());
assertEquals(toHashA2, toLocalRepo2.resolve("master").get());
pr.setState(Issue.State.CLOSED);
TestBotRunner.runPeriodicItems(bot2);
toCommitsAfterMerge2 = toLocalRepo2.commits().asList();
assertEquals(2, toCommitsAfterMerge2.size());
assertEquals(fromHashB2, toCommitsAfterMerge2.get(0).hash());
assertEquals(toHashA2, toCommitsAfterMerge2.get(1).hash());
assertEquals(fromHashB2, toLocalRepo2.resolve("master").get());
}
}
@Test
void failingMergeShouldResultInOnlyOnePR(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var toGitConfig = toDir.resolve(".git").resolve("config");
Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var forkDir = temp.path().resolve("fork.git");
var forkLocalRepo = TestableRepository.init(forkDir, VCS.GIT);
var forkGitConfig = forkDir.resolve(".git").resolve("config");
Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo);
var now = ZonedDateTime.now();
var fromFileA = fromDir.resolve("a.txt");
Files.writeString(fromFileA, "Hello A\n");
fromLocalRepo.add(fromFileA);
var fromHashA = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(fromHashA, fromCommits.get(0).hash());
var toFileA = toDir.resolve("a.txt");
Files.writeString(toFileA, "Hello A\n");
toLocalRepo.add(toFileA);
var toHashA = toLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(toHashA, toCommits.get(0).hash());
assertEquals(fromHashA, toHashA);
var fromFileB = fromDir.resolve("b.txt");
Files.writeString(fromFileB, "Hello B1\n");
fromLocalRepo.add(fromFileB);
var fromHashB = fromLocalRepo.commit("Adding b1.txt", "duke", "duke@openjdk.org");
var toFileB = toDir.resolve("b.txt");
Files.writeString(toFileB, "Hello B2\n");
toLocalRepo.add(toFileB);
var toHashB = toLocalRepo.commit("Adding b2.txt", "duke", "duke@openjdk.org");
var storage = temp.path().resolve("storage");
var master = new Branch("master");
var specs = List.of(new MergeBot.Spec(fromHostedRepo, master, master));
var bot = new MergeBot(storage, toHostedRepo, toFork, specs);
TestBotRunner.runPeriodicItems(bot);
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(2, toCommits.size());
var toHashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
assertTrue(toHashes.contains(toHashA));
assertTrue(toHashes.contains(toHashB));
var pullRequests = toHostedRepo.openPullRequests();
assertEquals(1, pullRequests.size());
var pr = pullRequests.get(0);
assertEquals("Merge test:master", pr.title());
}
}
final static class TestClock implements Clock {
ZonedDateTime now;
TestClock() {
this(null);
}
TestClock(ZonedDateTime now) {
this.now = now;
}
@Override
public ZonedDateTime now() {
return now;
}
}
@Test
void testMergeHourly(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var toGitConfig = toDir.resolve(".git").resolve("config");
Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var forkDir = temp.path().resolve("fork.git");
var forkLocalRepo = TestableRepository.init(forkDir, VCS.GIT);
var forkGitConfig = forkDir.resolve(".git").resolve("config");
Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo);
var now = ZonedDateTime.now();
var fromFileA = fromDir.resolve("a.txt");
Files.writeString(fromFileA, "Hello A\n");
fromLocalRepo.add(fromFileA);
var fromHashA = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(fromHashA, fromCommits.get(0).hash());
var toFileA = toDir.resolve("a.txt");
Files.writeString(toFileA, "Hello A\n");
toLocalRepo.add(toFileA);
var toHashA = toLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(toHashA, toCommits.get(0).hash());
assertEquals(fromHashA, toHashA);
var fromFileB = fromDir.resolve("b.txt");
Files.writeString(fromFileB, "Hello B\n");
fromLocalRepo.add(fromFileB);
var fromHashB = fromLocalRepo.commit("Adding b.txt", "duke", "duke@openjdk.org");
var toFileC = toDir.resolve("c.txt");
Files.writeString(toFileC, "Hello C\n");
toLocalRepo.add(toFileC);
var toHashC = toLocalRepo.commit("Adding c.txt", "duke", "duke@openjdk.org");
var storage = temp.path().resolve("storage");
var master = new Branch("master");
// Merge only at most once during the first minute every hour
var freq = MergeBot.Spec.Frequency.hourly(1);
var specs = List.of(new MergeBot.Spec(fromHostedRepo, master, master, freq));
var clock = new TestClock(ZonedDateTime.of(2020, 1, 23, 15, 0, 0, 0, ZoneId.of("GMT+1")));
var bot = new MergeBot(storage, toHostedRepo, toFork, specs, clock);
TestBotRunner.runPeriodicItems(bot);
// Ensure nothing has been merged
toCommits = toLocalRepo.commits().asList();
assertEquals(2, toCommits.size());
assertEquals(toHashC, toCommits.get(0).hash());
assertEquals(toHashA, toCommits.get(1).hash());
// Set the clock to the first minute of the hour
clock.now = ZonedDateTime.of(2020, 1, 23, 15, 1, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
// Should have merged
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
var hashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
assertTrue(hashes.contains(toHashA));
assertTrue(hashes.contains(fromHashB));
assertTrue(hashes.contains(toHashC));
var known = Set.of(toHashA, fromHashB, toHashC);
var merge = toCommits.stream().filter(c -> !known.contains(c.hash())).findAny().get();
assertTrue(merge.isMerge());
assertEquals(List.of("Automatic merge of test:master into master"), merge.message());
assertEquals("duke", merge.author().name());
assertEquals("duke@openjdk.org", merge.author().email());
assertEquals(0, toHostedRepo.openPullRequests().size());
var fromFileD = fromDir.resolve("d.txt");
Files.writeString(fromFileD, "Hello D\n");
fromLocalRepo.add(fromFileD);
var fromHashD = fromLocalRepo.commit("Adding d.txt", "duke", "duke@openjdk.org");
// Since the time hasn't changed it should not merge again
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the minutes forward, the bot should not merge
clock.now = ZonedDateTime.of(2020, 1, 23, 15, 45, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the clock forward one hour, the bot should merge
clock.now = ZonedDateTime.of(2020, 1, 23, 16, 1, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(6, toCommits.size());
}
}
@Test
void testMergeDaily(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var toGitConfig = toDir.resolve(".git").resolve("config");
Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test", toLocalRepo);
var forkDir = temp.path().resolve("fork.git");
var forkLocalRepo = TestableRepository.init(forkDir, VCS.GIT);
var forkGitConfig = forkDir.resolve(".git").resolve("config");
Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo);
var now = ZonedDateTime.now();
var fromFileA = fromDir.resolve("a.txt");
Files.writeString(fromFileA, "Hello A\n");
fromLocalRepo.add(fromFileA);
var fromHashA = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(fromHashA, fromCommits.get(0).hash());
var toFileA = toDir.resolve("a.txt");
Files.writeString(toFileA, "Hello A\n");
toLocalRepo.add(toFileA);
var toHashA = toLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(toHashA, toCommits.get(0).hash());
assertEquals(fromHashA, toHashA);
var fromFileB = fromDir.resolve("b.txt");
Files.writeString(fromFileB, "Hello B\n");
fromLocalRepo.add(fromFileB);
var fromHashB = fromLocalRepo.commit("Adding b.txt", "duke", "duke@openjdk.org");
var toFileC = toDir.resolve("c.txt");
Files.writeString(toFileC, "Hello C\n");
toLocalRepo.add(toFileC);
var toHashC = toLocalRepo.commit("Adding c.txt", "duke", "duke@openjdk.org");
var storage = temp.path().resolve("storage");
var master = new Branch("master");
// Merge only at most once during the third hour every day
var freq = MergeBot.Spec.Frequency.daily(3);
var specs = List.of(new MergeBot.Spec(fromHostedRepo, master, master, freq));
var clock = new TestClock(ZonedDateTime.of(2020, 1, 23, 2, 45, 0, 0, ZoneId.of("GMT+1")));
var bot = new MergeBot(storage, toHostedRepo, toFork, specs, clock);
TestBotRunner.runPeriodicItems(bot);
// Ensure nothing has been merged
toCommits = toLocalRepo.commits().asList();
assertEquals(2, toCommits.size());
assertEquals(toHashC, toCommits.get(0).hash());
assertEquals(toHashA, toCommits.get(1).hash());
// Set the clock to the third hour of the day (minutes should not matter)
clock.now = ZonedDateTime.of(2020, 1, 23, 3, 37, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
// Should have merged
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
var hashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
assertTrue(hashes.contains(toHashA));
assertTrue(hashes.contains(fromHashB));
assertTrue(hashes.contains(toHashC));
var known = Set.of(toHashA, fromHashB, toHashC);
var merge = toCommits.stream().filter(c -> !known.contains(c.hash())).findAny().get();
assertTrue(merge.isMerge());
assertEquals(List.of("Automatic merge of master into master"), merge.message());
assertEquals("duke", merge.author().name());
assertEquals("duke@openjdk.org", merge.author().email());
assertEquals(0, toHostedRepo.openPullRequests().size());
var fromFileD = fromDir.resolve("d.txt");
Files.writeString(fromFileD, "Hello D\n");
fromLocalRepo.add(fromFileD);
var fromHashD = fromLocalRepo.commit("Adding d.txt", "duke", "duke@openjdk.org");
// Since the time hasn't changed it should not merge
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the minutes forward, the bot should not merge
clock.now = ZonedDateTime.of(2020, 1, 23, 3, 45, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the hours forward, the bot should not merge
clock.now = ZonedDateTime.of(2020, 1, 23, 17, 45, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the clock forward one day, the bot should merge
clock.now = ZonedDateTime.of(2020, 1, 24, 3, 55, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(6, toCommits.size());
}
}
@Test
void testMergeWeekly(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var toGitConfig = toDir.resolve(".git").resolve("config");
Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var forkDir = temp.path().resolve("fork.git");
var forkLocalRepo = TestableRepository.init(forkDir, VCS.GIT);
var forkGitConfig = forkDir.resolve(".git").resolve("config");
Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo);
var now = ZonedDateTime.now();
var fromFileA = fromDir.resolve("a.txt");
Files.writeString(fromFileA, "Hello A\n");
fromLocalRepo.add(fromFileA);
var fromHashA = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(fromHashA, fromCommits.get(0).hash());
var toFileA = toDir.resolve("a.txt");
Files.writeString(toFileA, "Hello A\n");
toLocalRepo.add(toFileA);
var toHashA = toLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(toHashA, toCommits.get(0).hash());
assertEquals(fromHashA, toHashA);
var fromFileB = fromDir.resolve("b.txt");
Files.writeString(fromFileB, "Hello B\n");
fromLocalRepo.add(fromFileB);
var fromHashB = fromLocalRepo.commit("Adding b.txt", "duke", "duke@openjdk.org");
var toFileC = toDir.resolve("c.txt");
Files.writeString(toFileC, "Hello C\n");
toLocalRepo.add(toFileC);
var toHashC = toLocalRepo.commit("Adding c.txt", "duke", "duke@openjdk.org");
var storage = temp.path().resolve("storage");
var master = new Branch("master");
// Merge only at most once per week on Friday's at 12:00
var freq = MergeBot.Spec.Frequency.weekly(DayOfWeek.FRIDAY, 12);
var specs = List.of(new MergeBot.Spec(fromHostedRepo, master, master, freq));
var clock = new TestClock(ZonedDateTime.of(2020, 1, 24, 11, 45, 0, 0, ZoneId.of("GMT+1")));
var bot = new MergeBot(storage, toHostedRepo, toFork, specs, clock);
TestBotRunner.runPeriodicItems(bot);
// Ensure nothing has been merged
toCommits = toLocalRepo.commits().asList();
assertEquals(2, toCommits.size());
assertEquals(toHashC, toCommits.get(0).hash());
assertEquals(toHashA, toCommits.get(1).hash());
// Set the clock to the 12th hour of the day (minutes should not matter)
clock.now = ZonedDateTime.of(2020, 1, 24, 12, 37, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
// Should have merged
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
var hashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
assertTrue(hashes.contains(toHashA));
assertTrue(hashes.contains(fromHashB));
assertTrue(hashes.contains(toHashC));
var known = Set.of(toHashA, fromHashB, toHashC);
var merge = toCommits.stream().filter(c -> !known.contains(c.hash())).findAny().get();
assertTrue(merge.isMerge());
assertEquals(List.of("Automatic merge of test:master into master"), merge.message());
assertEquals("duke", merge.author().name());
assertEquals("duke@openjdk.org", merge.author().email());
assertEquals(0, toHostedRepo.openPullRequests().size());
var fromFileD = fromDir.resolve("d.txt");
Files.writeString(fromFileD, "Hello D\n");
fromLocalRepo.add(fromFileD);
var fromHashD = fromLocalRepo.commit("Adding d.txt", "duke", "duke@openjdk.org");
// Since the time hasn't changed it should not merge
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the hours forward, the bot should not merge
clock.now = ZonedDateTime.of(2020, 1, 24, 13, 45, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the days forward, the bot should not merge
clock.now = ZonedDateTime.of(2020, 1, 25, 13, 45, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the clock forward one week, the bot should merge
clock.now = ZonedDateTime.of(2020, 1, 31, 12, 29, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(6, toCommits.size());
}
}
@Test
void testMergeMonthly(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var toGitConfig = toDir.resolve(".git").resolve("config");
Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test", toLocalRepo);
var forkDir = temp.path().resolve("fork.git");
var forkLocalRepo = TestableRepository.init(forkDir, VCS.GIT);
var forkGitConfig = forkDir.resolve(".git").resolve("config");
Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo);
var now = ZonedDateTime.now();
var fromFileA = fromDir.resolve("a.txt");
Files.writeString(fromFileA, "Hello A\n");
fromLocalRepo.add(fromFileA);
var fromHashA = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(fromHashA, fromCommits.get(0).hash());
var toFileA = toDir.resolve("a.txt");
Files.writeString(toFileA, "Hello A\n");
toLocalRepo.add(toFileA);
var toHashA = toLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(toHashA, toCommits.get(0).hash());
assertEquals(fromHashA, toHashA);
var fromFileB = fromDir.resolve("b.txt");
Files.writeString(fromFileB, "Hello B\n");
fromLocalRepo.add(fromFileB);
var fromHashB = fromLocalRepo.commit("Adding b.txt", "duke", "duke@openjdk.org");
var toFileC = toDir.resolve("c.txt");
Files.writeString(toFileC, "Hello C\n");
toLocalRepo.add(toFileC);
var toHashC = toLocalRepo.commit("Adding c.txt", "duke", "duke@openjdk.org");
var storage = temp.path().resolve("storage");
var master = new Branch("master");
// Merge only at most once per month on the 17th day at at 11:00
var freq = MergeBot.Spec.Frequency.monthly(17, 11);
var specs = List.of(new MergeBot.Spec(fromHostedRepo, master, master, freq));
var clock = new TestClock(ZonedDateTime.of(2020, 1, 16, 11, 0, 0, 0, ZoneId.of("GMT+1")));
var bot = new MergeBot(storage, toHostedRepo, toFork, specs, clock);
TestBotRunner.runPeriodicItems(bot);
// Ensure nothing has been merged
toCommits = toLocalRepo.commits().asList();
assertEquals(2, toCommits.size());
assertEquals(toHashC, toCommits.get(0).hash());
assertEquals(toHashA, toCommits.get(1).hash());
// Set the clock to the 17th day and at hour 11 (minutes should not matter)
clock.now = ZonedDateTime.of(2020, 1, 17, 11, 37, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
// Should have merged
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
var hashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
assertTrue(hashes.contains(toHashA));
assertTrue(hashes.contains(fromHashB));
assertTrue(hashes.contains(toHashC));
var known = Set.of(toHashA, fromHashB, toHashC);
var merge = toCommits.stream().filter(c -> !known.contains(c.hash())).findAny().get();
assertTrue(merge.isMerge());
assertEquals(List.of("Automatic merge of master into master"), merge.message());
assertEquals("duke", merge.author().name());
assertEquals("duke@openjdk.org", merge.author().email());
assertEquals(0, toHostedRepo.openPullRequests().size());
var fromFileD = fromDir.resolve("d.txt");
Files.writeString(fromFileD, "Hello D\n");
fromLocalRepo.add(fromFileD);
var fromHashD = fromLocalRepo.commit("Adding d.txt", "duke", "duke@openjdk.org");
// Since the time hasn't changed it should not merge
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the hours forward, the bot should not merge
clock.now = ZonedDateTime.of(2020, 1, 17, 12, 45, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the days forward, the bot should not merge
clock.now = ZonedDateTime.of(2020, 1, 18, 11, 0, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the clock forward one month, the bot should merge
clock.now = ZonedDateTime.of(2020, 2, 17, 11, 55, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(6, toCommits.size());
}
}
@Test
void testMergeYearly(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var toGitConfig = toDir.resolve(".git").resolve("config");
Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test", toLocalRepo);
var forkDir = temp.path().resolve("fork.git");
var forkLocalRepo = TestableRepository.init(forkDir, VCS.GIT);
var forkGitConfig = forkDir.resolve(".git").resolve("config");
Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo);
var now = ZonedDateTime.now();
var fromFileA = fromDir.resolve("a.txt");
Files.writeString(fromFileA, "Hello A\n");
fromLocalRepo.add(fromFileA);
var fromHashA = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(fromHashA, fromCommits.get(0).hash());
var toFileA = toDir.resolve("a.txt");
Files.writeString(toFileA, "Hello A\n");
toLocalRepo.add(toFileA);
var toHashA = toLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(toHashA, toCommits.get(0).hash());
assertEquals(fromHashA, toHashA);
var fromFileB = fromDir.resolve("b.txt");
Files.writeString(fromFileB, "Hello B\n");
fromLocalRepo.add(fromFileB);
var fromHashB = fromLocalRepo.commit("Adding b.txt", "duke", "duke@openjdk.org");
var toFileC = toDir.resolve("c.txt");
Files.writeString(toFileC, "Hello C\n");
toLocalRepo.add(toFileC);
var toHashC = toLocalRepo.commit("Adding c.txt", "duke", "duke@openjdk.org");
var storage = temp.path().resolve("storage");
var master = new Branch("master");
// Merge only at most once per year on the 29th day of May at at 07:00
var freq = MergeBot.Spec.Frequency.yearly(Month.MAY, 29, 07);
var specs = List.of(new MergeBot.Spec(fromHostedRepo, master, master, freq));
var clock = new TestClock(ZonedDateTime.of(2020, 5, 27, 11, 0, 0, 0, ZoneId.of("GMT+1")));
var bot = new MergeBot(storage, toHostedRepo, toFork, specs, clock);
TestBotRunner.runPeriodicItems(bot);
// Ensure nothing has been merged
toCommits = toLocalRepo.commits().asList();
assertEquals(2, toCommits.size());
assertEquals(toHashC, toCommits.get(0).hash());
assertEquals(toHashA, toCommits.get(1).hash());
// Set the clock to the 29th of May and at hour 11 (minutes should not matter)
clock.now = ZonedDateTime.of(2020, 5, 29, 7, 37, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
// Should have merged
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
var hashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
assertTrue(hashes.contains(toHashA));
assertTrue(hashes.contains(fromHashB));
assertTrue(hashes.contains(toHashC));
var known = Set.of(toHashA, fromHashB, toHashC);
var merge = toCommits.stream().filter(c -> !known.contains(c.hash())).findAny().get();
assertTrue(merge.isMerge());
assertEquals(List.of("Automatic merge of master into master"), merge.message());
assertEquals("duke", merge.author().name());
assertEquals("duke@openjdk.org", merge.author().email());
assertEquals(0, toHostedRepo.openPullRequests().size());
var fromFileD = fromDir.resolve("d.txt");
Files.writeString(fromFileD, "Hello D\n");
fromLocalRepo.add(fromFileD);
var fromHashD = fromLocalRepo.commit("Adding d.txt", "duke", "duke@openjdk.org");
// Since the time hasn't changed it should not merge again
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the hours forward, the bot should not merge
clock.now = ZonedDateTime.of(2020, 5, 29, 8, 45, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the days forward, the bot should not merge
clock.now = ZonedDateTime.of(2020, 5, 30, 11, 0, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the months forward, the bot should not merge
clock.now = ZonedDateTime.of(2020, 7, 29, 7, 0, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(4, toCommits.size());
// Move the clock forward one year, the bot should merge
clock.now = ZonedDateTime.of(2021, 5, 29, 7, 55, 0, 0, ZoneId.of("GMT+1"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(6, toCommits.size());
}
}
@Test
void mergeAfterDivergedStorage(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var toGitConfig = toDir.resolve(".git").resolve("config");
Files.write(toGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var forkDir = temp.path().resolve("fork.git");
var forkLocalRepo = TestableRepository.init(forkDir, VCS.GIT);
var forkGitConfig = forkDir.resolve(".git").resolve("config");
Files.write(forkGitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toFork = new TestHostedRepository(host, "test-mirror-fork", forkLocalRepo);
var now = ZonedDateTime.now();
var fromFileA = fromDir.resolve("a.txt");
Files.writeString(fromFileA, "Hello A\n");
fromLocalRepo.add(fromFileA);
var fromHashA = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(fromHashA, fromCommits.get(0).hash());
var toFileA = toDir.resolve("a.txt");
Files.writeString(toFileA, "Hello A\n");
toLocalRepo.add(toFileA);
var toHashA = toLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
var toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(toHashA, toCommits.get(0).hash());
assertEquals(fromHashA, toHashA);
var storage = temp.path().resolve("storage");
var master = new Branch("master");
var specs = List.of(new MergeBot.Spec(fromHostedRepo, master, master));
var bot = new MergeBot(storage, toHostedRepo, toFork, specs);
TestBotRunner.runPeriodicItems(bot);
// Add something new to the source
var fromFileB = fromDir.resolve("b.txt");
Files.writeString(fromFileB, "Hello B\n");
fromLocalRepo.add(fromFileB);
var fromHashB = fromLocalRepo.commit("Adding a.txt", "duke", "duke@openjdk.org", now);
fromLocalRepo.push(fromHashB, fromHostedRepo.authenticatedUrl(), "master");
// Diverge the target with something non-conflicting
var toFileC = toDir.resolve("c.txt");
Files.writeString(toFileC, "Hello C\n");
toLocalRepo.add(toFileC);
var toHashC = toLocalRepo.commit("Adding c.txt", "duke", "duke@openjdk.org");
toLocalRepo.push(toHashC, toHostedRepo.authenticatedUrl(), "master");
// But push something out of place to the local storage as well
var sanitizedForkUrl = URLEncoder.encode(toFork.webUrl().toString(), StandardCharsets.UTF_8);
var storageRepo = TestableRepository.init(storage.resolve(sanitizedForkUrl), VCS.GIT);
var divergedForkFile = storageRepo.root().resolve("d.txt");
Files.writeString(divergedForkFile, "Hello D\n");
storageRepo.add(divergedForkFile);
var divergedForkHash = storageRepo.commit("Adding d.txt", "duke", "duke@openjdk.org");
// This will need manual intervention
assertThrows(RuntimeException.class, () -> TestBotRunner.runPeriodicItems(bot));
}
}
}
================================================
FILE: bots/mirror/build.gradle
================================================
/*
* Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module {
name = 'org.openjdk.skara.bots.mirror'
test {
requires 'org.junit.jupiter.api'
requires 'org.openjdk.skara.test'
opens 'org.openjdk.skara.bots.mirror' to 'org.junit.platform.commons'
}
}
dependencies {
implementation project(':ci')
implementation project(':host')
implementation project(':forge')
implementation project(':issuetracker')
implementation project(':bot')
implementation project(':census')
implementation project(':json')
implementation project(':vcs')
implementation project(':metrics')
testImplementation project(':test')
}
================================================
FILE: bots/mirror/src/main/java/module-info.java
================================================
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module org.openjdk.skara.bots.mirror {
requires org.openjdk.skara.bot;
requires org.openjdk.skara.vcs;
requires java.logging;
provides org.openjdk.skara.bot.BotFactory with org.openjdk.skara.bots.mirror.MirrorBotFactory;
}
================================================
FILE: bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBot.java
================================================
/*
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.mirror;
import java.util.regex.Pattern;
import org.openjdk.skara.bot.*;
import org.openjdk.skara.forge.HostedRepository;
import org.openjdk.skara.vcs.*;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
* The MirrorBot mirrors one HostedRepository to another. It can be configured
* to only mirror a specific set of branches, or everything (which also
* includes tags). When only mirroring a set of branches, the includeTags
* setting can be used to also include tags.
*/
class MirrorBot implements Bot, WorkItem {
private final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
private final Path storage;
private final HostedRepository from;
private final HostedRepository to;
private final List branchPatterns;
private final boolean includeTags;
private final boolean onlyTags;
private final List refspecs;
MirrorBot(Path storage, HostedRepository from, HostedRepository to) {
this(storage, from, to, List.of(), true, false, List.of());
}
MirrorBot(Path storage, HostedRepository from, HostedRepository to, List branchPatterns,
boolean includeTags, boolean onlyTags, List refspecs) {
this.storage = storage;
this.from = from;
this.to = to;
this.branchPatterns = branchPatterns;
this.includeTags = includeTags;
this.onlyTags = onlyTags;
this.refspecs = refspecs;
}
@Override
public boolean concurrentWith(WorkItem other) {
if (!(other instanceof MirrorBot otherBot)) {
return true;
}
return !to.name().equals(otherBot.to.name());
}
@Override
public Collection run(Path scratchPath) {
try {
var sanitizedUrl =
URLEncoder.encode(to.webUrl().toString(), StandardCharsets.UTF_8);
var dir = storage.resolve(sanitizedUrl);
Repository repo = null;
if (!Files.exists(dir)) {
log.info("Cloning " + from.name());
Files.createDirectories(dir);
repo = Repository.mirror(from.authenticatedUrl(), dir);
} else {
log.info("Found existing scratch directory for " + to.name());
repo = Repository.get(dir).orElseGet(() -> {
log.info("The existing scratch directory is not a valid repository. Recloning " + from.name());
try {
try (var paths = Files.walk(dir)) {
paths.map(Path::toFile)
.sorted(Comparator.reverseOrder())
.forEach(File::delete);
}
return Repository.mirror(from.authenticatedUrl(), dir);
} catch (IOException io) {
throw new RuntimeException(io);
}
});
}
log.info("Pulling " + from.name());
repo.fetchAll(from.authenticatedUrl(), includeTags || onlyTags || !refspecs.isEmpty());
if (onlyTags) {
log.info("Pushing tags to " + to.name());
repo.pushTags(to.authenticatedUrl(), true);
} else if (branchPatterns.isEmpty() && includeTags) {
log.info("Pushing tags and branches to " + to.name());
repo.pushAll(to.authenticatedUrl(), true);
} else if (!branchPatterns.isEmpty()) {
for (var branch : repo.branches()) {
if (branchPatterns.stream().anyMatch(p -> p.matcher(branch.name()).matches())) {
var hash = repo.resolve(branch);
if (hash.isPresent()) {
log.info("Pushing branch " + branch.name() + " to " + to.name() + " " +
(includeTags ? "including" : "excluding") + " tags");
repo.push(hash.get(), to.authenticatedUrl(), branch.name(), true, includeTags);
} else {
log.severe("Branch " + branch + " not found in repo " + repo);
}
}
}
} else if (!refspecs.isEmpty()) {
for (var refspec : refspecs) {
log.info("Pushing using refspec " + refspec + " to " + to.name());
repo.push(refspec, to.authenticatedUrl());
}
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
return List.of();
}
@Override
public String toString() {
var name = "MirrorBot@" + from.name() + "->" + to.name();
if (!refspecs.isEmpty()) {
name += " (" + String.join(",", refspecs) + ")";
} else {
if (branchPatterns.isEmpty()) {
if (onlyTags) {
name += " ()";
} else {
name += " (*)";
}
} else {
var branchPatterns = this.branchPatterns.stream().map(Pattern::toString).collect(Collectors.toList());
name += " (" + String.join(",", branchPatterns) + ")";
}
if (onlyTags) {
name += " [tags only]";
} else if (includeTags) {
name += " [tags included]";
} else {
name += " [tags excluded]";
}
}
return name;
}
@Override
public List getPeriodicItems() {
return List.of(this);
}
@Override
public String workItemName() {
return botName();
}
@Override
public String botName() {
return name();
}
@Override
public String name() {
return MirrorBotFactory.NAME;
}
public List getBranchPatterns() {
return branchPatterns;
}
public boolean isIncludeTags() {
return includeTags;
}
public boolean isOnlyTags() {
return onlyTags;
}
public List getRefspecs() {
return refspecs;
}
}
================================================
FILE: bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBotFactory.java
================================================
/*
* Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.mirror;
import java.util.regex.Pattern;
import org.openjdk.skara.bot.*;
import org.openjdk.skara.json.JSONValue;
import org.openjdk.skara.vcs.Branch;
import java.io.*;
import java.nio.file.Files;
import java.util.*;
import java.util.stream.Collectors;
import java.util.logging.Logger;
public class MirrorBotFactory implements BotFactory {
private final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
static final String NAME = "mirror";
@Override
public String name() {
return NAME;
}
@Override
public List create(BotConfiguration configuration) {
var storage = configuration.storageFolder();
try {
Files.createDirectories(storage);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
var specific = configuration.specific();
var bots = new ArrayList();
for (var repo : specific.get("repositories").asArray()) {
var fromName = repo.get("from").asString();
var fromRepo = configuration.repository(fromName);
var toName = repo.get("to").asString();
var toRepo = configuration.repository(toName);
List refspecs;
if (repo.contains("refspecs")) {
var refspecsElement = repo.get("refspecs");
if (refspecsElement.isArray()) {
refspecs = refspecsElement.asArray().stream()
.map(JSONValue::asString)
.toList();
} else {
refspecs = List.of(refspecsElement.asString());
}
} else {
refspecs = List.of();
}
List branchPatterns;
if (repo.contains("branches")) {
if (!refspecs.isEmpty()) {
throw new IllegalStateException("Cannot combine refspecs and branches");
}
// Accept both an array of regex patterns as well as a single comma separated
// string for backwards compatibility
var branchesElement = repo.get("branches");
if (branchesElement.isArray()) {
branchPatterns = branchesElement.asArray().stream()
.map(JSONValue::asString)
.map(Pattern::compile)
.toList();
} else {
branchPatterns = Arrays.stream(repo.get("branches").asString().split(","))
.map(Pattern::compile)
.toList();
}
} else {
branchPatterns = List.of();
}
var includeTags = branchPatterns.isEmpty() && refspecs.isEmpty();
var onlyTags = false;
if (repo.contains("tags")) {
var tags = repo.get("tags").asString().toLowerCase().strip();
if (!Set.of("include", "only").contains(tags)) {
throw new IllegalStateException("\"tags\" field can only have value \"include\" or \"only\"");
}
onlyTags = tags.equals("only");
includeTags = tags.equals("include");
}
if (onlyTags) {
// Tags are by definition included when only tags are mirrored
includeTags = true;
}
if (onlyTags && !branchPatterns.isEmpty()) {
throw new IllegalStateException("Branches cannot be mirrored when only tags are mirrored");
}
if ((onlyTags || includeTags) && !refspecs.isEmpty()) {
throw new IllegalStateException("Cannot combine refspecs and tags");
}
log.info("Setting up mirroring from " + fromRepo.name() + " to " + toRepo.name());
bots.add(new MirrorBot(storage, fromRepo, toRepo, branchPatterns, includeTags, onlyTags, refspecs));
}
return bots;
}
}
================================================
FILE: bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotFactoryTest.java
================================================
/*
* Copyright (c) 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.mirror;
import java.util.List;
import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
import org.openjdk.skara.json.JWCC;
import org.openjdk.skara.test.TemporaryDirectory;
import org.openjdk.skara.test.TestBotFactory;
import org.openjdk.skara.test.TestHostedRepository;
import static org.junit.jupiter.api.Assertions.*;
class MirrorBotFactoryTest {
@Test
public void testCreate() {
try (var tempFolder = new TemporaryDirectory()) {
String jsonString = """
{
"repositories": [
{
"from": "from1",
"to": "to1",
"branches": "master"
},
{
"from": "from2",
"to": "to2",
"branches": [
"master",
"dev",
"test"
]
},
{
"from": "from3",
"to": "to3"
},
]
}
""";
var jsonConfig = JWCC.parse(jsonString).asObject();
var testBotFactory = TestBotFactory.newBuilder()
.addHostedRepository("from1", new TestHostedRepository("from1"))
.addHostedRepository("from2", new TestHostedRepository("from2"))
.addHostedRepository("from3", new TestHostedRepository("from3"))
.addHostedRepository("to1", new TestHostedRepository("to1"))
.addHostedRepository("to2", new TestHostedRepository("to2"))
.addHostedRepository("to3", new TestHostedRepository("to3"))
.storagePath(tempFolder.path().resolve("storage"))
.build();
var bots = testBotFactory.createBots(MirrorBotFactory.NAME, jsonConfig);
assertEquals(3, bots.size());
MirrorBot mirrorBot1 = (MirrorBot) bots.get(0);
assertEquals("MirrorBot@from1->to1 (master) [tags excluded]", mirrorBot1.toString());
assertFalse(mirrorBot1.isIncludeTags());
assertFalse(mirrorBot1.isOnlyTags());
assertEquals("master", mirrorBot1.getBranchPatterns().get(0).toString());
MirrorBot mirrorBot2 = (MirrorBot) bots.get(1);
assertEquals("MirrorBot@from2->to2 (master,dev,test) [tags excluded]", mirrorBot2.toString());
assertFalse(mirrorBot2.isIncludeTags());
assertFalse(mirrorBot2.isOnlyTags());
assertEquals("master", mirrorBot2.getBranchPatterns().get(0).toString());
assertEquals("dev", mirrorBot2.getBranchPatterns().get(1).toString());
assertEquals("test", mirrorBot2.getBranchPatterns().get(2).toString());
MirrorBot mirrorBot3 = (MirrorBot) bots.get(2);
assertEquals("MirrorBot@from3->to3 (*) [tags included]", mirrorBot3.toString());
assertTrue(mirrorBot3.isIncludeTags());
assertFalse(mirrorBot3.isOnlyTags());
assertEquals(0, mirrorBot3.getBranchPatterns().size());
}
}
@Test
public void testThrowsWithUnsupportedTagsValue() {
try (var tempFolder = new TemporaryDirectory()) {
String jsonString = """
{
"repositories": [
{
"from": "from1",
"to": "to1",
"branches": "master",
"tags": "foo"
}
]
}
""";
var jsonConfig = JWCC.parse(jsonString).asObject();
var testBotFactory = TestBotFactory.newBuilder()
.addHostedRepository("from1", new TestHostedRepository("from1"))
.addHostedRepository("to1", new TestHostedRepository("to1"))
.storagePath(tempFolder.path().resolve("storage"))
.build();
assertThrows(IllegalStateException.class, () -> testBotFactory.createBots(MirrorBotFactory.NAME, jsonConfig));
}
}
@Test
public void testThrowsWithBranchesAndTagsOnly() {
try (var tempFolder = new TemporaryDirectory()) {
String jsonString = """
{
"repositories": [
{
"from": "from1",
"to": "to1",
"branches": "master",
"tags": "only"
}
]
}
""";
var jsonConfig = JWCC.parse(jsonString).asObject();
var testBotFactory = TestBotFactory.newBuilder()
.addHostedRepository("from1", new TestHostedRepository("from1"))
.addHostedRepository("to1", new TestHostedRepository("to1"))
.storagePath(tempFolder.path().resolve("storage"))
.build();
assertThrows(IllegalStateException.class, () -> testBotFactory.createBots(MirrorBotFactory.NAME, jsonConfig));
}
}
@Test
public void testCreateWithTags() {
try (var tempFolder = new TemporaryDirectory()) {
String jsonString = """
{
"repositories": [
{
"from": "from1",
"to": "to1",
"branches": "master"
},
{
"from": "from2",
"to": "to2",
"tags": "include"
},
{
"from": "from3",
"to": "to3",
},
{
"from": "from4",
"to": "to4",
"tags": "only"
},
{
"from": "from5",
"to": "to5",
"branches": ["master", "dev"]
},
{
"from": "from6",
"to": "to6",
"branches": ["master", "dev"],
"tags": "include"
},
]
}
""";
var jsonConfig = JWCC.parse(jsonString).asObject();
var testBotFactory = TestBotFactory.newBuilder()
.addHostedRepository("from1", new TestHostedRepository("from1"))
.addHostedRepository("from2", new TestHostedRepository("from2"))
.addHostedRepository("from3", new TestHostedRepository("from3"))
.addHostedRepository("from4", new TestHostedRepository("from4"))
.addHostedRepository("from5", new TestHostedRepository("from5"))
.addHostedRepository("from6", new TestHostedRepository("from6"))
.addHostedRepository("to1", new TestHostedRepository("to1"))
.addHostedRepository("to2", new TestHostedRepository("to2"))
.addHostedRepository("to3", new TestHostedRepository("to3"))
.addHostedRepository("to4", new TestHostedRepository("to4"))
.addHostedRepository("to5", new TestHostedRepository("to5"))
.addHostedRepository("to6", new TestHostedRepository("to6"))
.storagePath(tempFolder.path().resolve("storage"))
.build();
var bots = testBotFactory.createBots(MirrorBotFactory.NAME, jsonConfig);
assertEquals(6, bots.size());
MirrorBot mirrorBot1 = (MirrorBot) bots.get(0);
assertEquals("MirrorBot@from1->to1 (master) [tags excluded]", mirrorBot1.toString());
assertFalse(mirrorBot1.isIncludeTags());
assertFalse(mirrorBot1.isOnlyTags());
assertEquals(List.of("master"),
mirrorBot1.getBranchPatterns().stream().map(Pattern::toString).toList());
MirrorBot mirrorBot2 = (MirrorBot) bots.get(1);
assertEquals("MirrorBot@from2->to2 (*) [tags included]", mirrorBot2.toString());
assertTrue(mirrorBot2.isIncludeTags());
assertFalse(mirrorBot2.isOnlyTags());
assertEquals(List.of(), mirrorBot2.getBranchPatterns());
MirrorBot mirrorBot3 = (MirrorBot) bots.get(2);
assertEquals("MirrorBot@from3->to3 (*) [tags included]", mirrorBot3.toString());
assertTrue(mirrorBot3.isIncludeTags());
assertFalse(mirrorBot3.isOnlyTags());
assertEquals(List.of(), mirrorBot3.getBranchPatterns());
MirrorBot mirrorBot4 = (MirrorBot) bots.get(3);
assertEquals("MirrorBot@from4->to4 () [tags only]", mirrorBot4.toString());
assertTrue(mirrorBot4.isIncludeTags());
assertTrue(mirrorBot4.isOnlyTags());
assertEquals(List.of(), mirrorBot4.getBranchPatterns());
MirrorBot mirrorBot5 = (MirrorBot) bots.get(4);
assertEquals("MirrorBot@from5->to5 (master,dev) [tags excluded]", mirrorBot5.toString());
assertFalse(mirrorBot5.isIncludeTags());
assertFalse(mirrorBot5.isOnlyTags());
assertEquals(List.of("master", "dev"),
mirrorBot5.getBranchPatterns().stream().map(Pattern::toString).toList());
MirrorBot mirrorBot6 = (MirrorBot) bots.get(5);
assertEquals("MirrorBot@from6->to6 (master,dev) [tags included]", mirrorBot6.toString());
assertTrue(mirrorBot6.isIncludeTags());
assertFalse(mirrorBot6.isOnlyTags());
assertEquals(List.of("master", "dev"),
mirrorBot6.getBranchPatterns().stream().map(Pattern::toString).toList());
}
}
@Test
public void testThrowsWithRefspecsAndTags() {
try (var tempFolder = new TemporaryDirectory()) {
String jsonString = """
{
"repositories": [
{
"from": "from1",
"to": "to1",
"refspecs": "refs/foo",
"tags": "only"
}
]
}
""";
var jsonConfig = JWCC.parse(jsonString).asObject();
var testBotFactory = TestBotFactory.newBuilder()
.addHostedRepository("from1", new TestHostedRepository("from1"))
.addHostedRepository("to1", new TestHostedRepository("to1"))
.storagePath(tempFolder.path().resolve("storage"))
.build();
assertThrows(IllegalStateException.class, () -> testBotFactory.createBots(MirrorBotFactory.NAME, jsonConfig));
}
}
@Test
public void testThrowsWithRefspecsAndBranches() {
try (var tempFolder = new TemporaryDirectory()) {
String jsonString = """
{
"repositories": [
{
"from": "from1",
"to": "to1",
"refspecs": "refs/foo",
"branches": "master"
}
]
}
""";
var jsonConfig = JWCC.parse(jsonString).asObject();
var testBotFactory = TestBotFactory.newBuilder()
.addHostedRepository("from1", new TestHostedRepository("from1"))
.addHostedRepository("to1", new TestHostedRepository("to1"))
.storagePath(tempFolder.path().resolve("storage"))
.build();
assertThrows(IllegalStateException.class, () -> testBotFactory.createBots(MirrorBotFactory.NAME, jsonConfig));
}
}
@Test
public void testCreateWithRefspecs() {
try (var tempFolder = new TemporaryDirectory()) {
String jsonString = """
{
"repositories": [
{
"from": "from1",
"to": "to1",
"refspecs": "refs/foo",
},
{
"from": "from2",
"to": "to2",
"refspecs": [
"refs/foo",
"refs/bar"
]
}
]
}
""";
var jsonConfig = JWCC.parse(jsonString).asObject();
var testBotFactory = TestBotFactory.newBuilder()
.addHostedRepository("from1", new TestHostedRepository("from1"))
.addHostedRepository("from2", new TestHostedRepository("from2"))
.addHostedRepository("to1", new TestHostedRepository("to1"))
.addHostedRepository("to2", new TestHostedRepository("to2"))
.storagePath(tempFolder.path().resolve("storage"))
.build();
var bots = testBotFactory.createBots(MirrorBotFactory.NAME, jsonConfig);
assertEquals(2, bots.size());
MirrorBot mirrorBot1 = (MirrorBot) bots.get(0);
assertEquals("MirrorBot@from1->to1 (refs/foo)", mirrorBot1.toString());
assertFalse(mirrorBot1.isIncludeTags());
assertFalse(mirrorBot1.isOnlyTags());
assertEquals(List.of(), mirrorBot1.getBranchPatterns());
assertEquals(List.of("refs/foo"), mirrorBot1.getRefspecs());
MirrorBot mirrorBot2 = (MirrorBot) bots.get(1);
assertEquals("MirrorBot@from2->to2 (refs/foo,refs/bar)", mirrorBot2.toString());
assertFalse(mirrorBot2.isIncludeTags());
assertFalse(mirrorBot2.isOnlyTags());
assertEquals(List.of(), mirrorBot2.getBranchPatterns());
assertEquals(List.of("refs/foo", "refs/bar"), mirrorBot2.getRefspecs());
}
}
}
================================================
FILE: bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotTests.java
================================================
/*
* Copyright (c) 2019, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.mirror;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.regex.Pattern;
import org.openjdk.skara.host.*;
import org.openjdk.skara.test.*;
import org.openjdk.skara.vcs.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class MirrorBotTests {
@Test
void mirrorMasterBranch(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var gitConfig = toDir.resolve(".git").resolve("config");
Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var newFile = fromDir.resolve("this-file-cannot-exist.txt");
Files.writeString(newFile, "Hello world\n");
fromLocalRepo.add(newFile);
var newHash = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(newHash, fromCommits.get(0).hash());
var toCommits = toLocalRepo.commits().asList();
assertEquals(0, toCommits.size());
var storage = temp.path().resolve("storage");
var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo);
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(newHash, toCommits.get(0).hash());
}
}
@Test
void mirrorMultipleBranches(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var gitConfig = toDir.resolve(".git").resolve("config");
Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var newFile = fromDir.resolve("this-file-cannot-exist.txt");
Files.writeString(newFile, "Hello world\n");
fromLocalRepo.add(newFile);
var newHash = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(newHash, fromCommits.get(0).hash());
fromLocalRepo.branch(newHash, "second");
fromLocalRepo.branch(newHash, "third");
var toCommits = toLocalRepo.commits().asList();
assertEquals(0, toCommits.size());
assertEquals(0, toLocalRepo.branches().size());
var storage = temp.path().resolve("storage");
var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo);
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(newHash, toCommits.get(0).hash());
var toBranches = toLocalRepo.branches();
assertEquals(3, toBranches.size());
assertTrue(toBranches.contains(new Branch("master")));
assertTrue(toBranches.contains(new Branch("second")));
assertTrue(toBranches.contains(new Branch("third")));
}
}
/**
* Tests mirrorEverything with multiple tags
*/
@Test
void mirrorEverythingMultipleTags(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var gitConfig = toDir.resolve(".git").resolve("config");
Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var newFile = fromDir.resolve("this-file-cannot-exist.txt");
Files.writeString(newFile, "Hello world\n");
fromLocalRepo.add(newFile);
var newHash = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(newHash, fromCommits.get(0).hash());
fromLocalRepo.tag(newHash, "first", "add first tag", "duke", "duk@openjdk.org");
fromLocalRepo.tag(newHash, "second", "add second tag", "duke", "duk@openjdk.org");
var toCommits = toLocalRepo.commits().asList();
assertEquals(0, toCommits.size());
assertEquals(0, toLocalRepo.tags().size());
var storage = temp.path().resolve("storage");
var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo);
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(newHash, toCommits.get(0).hash());
var toTags = toLocalRepo.tags();
assertEquals(2, toTags.size());
assertTrue(toTags.contains(new Tag("first")));
assertTrue(toTags.contains(new Tag("second")));
// Add another tag and go again
fromLocalRepo.tag(newHash, "third", "add third tag", "duke", "duk@openjdk.org");
TestBotRunner.runPeriodicItems(bot);
toTags = toLocalRepo.tags();
assertEquals(3, toTags.size());
assertTrue(toTags.contains(new Tag("first")));
assertTrue(toTags.contains(new Tag("second")));
assertTrue(toTags.contains(new Tag("third")));
}
}
/**
* Tests mirroring a single branch, including tags
*/
@Test
void mirrorSingleBranchAndTags(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var gitConfig = toDir.resolve(".git").resolve("config");
Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var newFile = fromDir.resolve("this-file-cannot-exist.txt");
Files.writeString(newFile, "Hello world\n");
fromLocalRepo.add(newFile);
var newHash = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(newHash, fromCommits.get(0).hash());
fromLocalRepo.tag(newHash, "first", "add first tag", "duke", "duk@openjdk.org");
fromLocalRepo.tag(newHash, "second", "add second tag", "duke", "duk@openjdk.org");
var toCommits = toLocalRepo.commits().asList();
assertEquals(0, toCommits.size());
assertEquals(0, toLocalRepo.tags().size());
var storage = temp.path().resolve("storage");
var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(Pattern.compile("master")), true, false, List.of());
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(newHash, toCommits.get(0).hash());
var toTags = toLocalRepo.tags();
assertEquals(2, toTags.size());
assertTrue(toTags.contains(new Tag("first")));
assertTrue(toTags.contains(new Tag("second")));
// Add another tag and go again
fromLocalRepo.tag(newHash, "third", "add third tag", "duke", "duk@openjdk.org");
TestBotRunner.runPeriodicItems(bot);
toTags = toLocalRepo.tags();
assertEquals(3, toTags.size());
assertTrue(toTags.contains(new Tag("first")));
assertTrue(toTags.contains(new Tag("second")));
assertTrue(toTags.contains(new Tag("third")));
// Change a tag and go again
Files.writeString(newFile, "Hello world again\n", StandardOpenOption.APPEND);
fromLocalRepo.add(newFile);
var secondHash = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var firstTag = fromLocalRepo.tag(secondHash, "first", "add first tag again", "duke", "duk@openjdk.org", null, true);
TestBotRunner.runPeriodicItems(bot);
toTags = toLocalRepo.tags();
assertEquals(3, toTags.size());
assertEquals(fromLocalRepo.annotate(firstTag), toLocalRepo.annotate(firstTag), "First tag not correctly mirrored");
}
}
/**
* Tests mirroring a single branch without including tags
*/
@Test
void mirrorSingleBranchNoTags(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var gitConfig = toDir.resolve(".git").resolve("config");
Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var newFile = fromDir.resolve("this-file-cannot-exist.txt");
Files.writeString(newFile, "Hello world\n");
fromLocalRepo.add(newFile);
var newHash = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(newHash, fromCommits.get(0).hash());
fromLocalRepo.tag(newHash, "first", "add first tag", "duke", "duk@openjdk.org");
var toCommits = toLocalRepo.commits().asList();
assertEquals(0, toCommits.size());
assertEquals(0, toLocalRepo.tags().size());
var storage = temp.path().resolve("storage");
var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(Pattern.compile("master")), false, false, List.of());
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(newHash, toCommits.get(0).hash());
var toTags = toLocalRepo.tags();
assertEquals(0, toTags.size());
// Go a second time
TestBotRunner.runPeriodicItems(bot);
toTags = toLocalRepo.tags();
assertEquals(0, toTags.size());
}
}
@Test
void mirrorRemovingBranch(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var gitConfig = toDir.resolve(".git").resolve("config");
Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var newFile = fromDir.resolve("this-file-cannot-exist.txt");
Files.writeString(newFile, "Hello world\n");
fromLocalRepo.add(newFile);
var newHash = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(newHash, fromCommits.get(0).hash());
fromLocalRepo.branch(newHash, "second");
fromLocalRepo.branch(newHash, "third");
var toCommits = toLocalRepo.commits().asList();
assertEquals(0, toCommits.size());
assertEquals(0, toLocalRepo.branches().size());
var storage = temp.path().resolve("storage");
var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo);
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(newHash, toCommits.get(0).hash());
var toBranches = toLocalRepo.branches();
assertEquals(3, toBranches.size());
assertTrue(toBranches.contains(new Branch("master")));
assertTrue(toBranches.contains(new Branch("second")));
assertTrue(toBranches.contains(new Branch("third")));
fromLocalRepo.delete(new Branch("second"));
assertEquals(2, fromLocalRepo.branches().size());
TestBotRunner.runPeriodicItems(bot);
toBranches = toLocalRepo.branches();
assertEquals(2, toBranches.size());
assertTrue(toBranches.contains(new Branch("master")));
assertTrue(toBranches.contains(new Branch("third")));
}
}
@Test
void mirrorSelectedBranches(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var gitConfig = toDir.resolve(".git").resolve("config");
Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var newFile = fromDir.resolve("this-file-cannot-exist.txt");
Files.writeString(newFile, "Hello world\n");
fromLocalRepo.add(newFile);
var first = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var featureBranch = fromLocalRepo.branch(first, "feature");
fromLocalRepo.checkout(featureBranch, false);
assertEquals(Optional.of(featureBranch), fromLocalRepo.currentBranch());
Files.writeString(newFile, "Hello again\n", StandardOpenOption.APPEND);
fromLocalRepo.add(newFile);
var second = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
assertEquals(Optional.of(first), fromLocalRepo.resolve("master"));
assertEquals(Optional.of(second), fromLocalRepo.resolve("feature"));
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(2, fromCommits.size());
var toCommits = toLocalRepo.commits().asList();
assertEquals(0, toCommits.size());
var storage = temp.path().resolve("storage");
var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(Pattern.compile("master")), false, false, List.of());
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(first, toCommits.get(0).hash());
assertEquals(List.of(new Branch("master")), toLocalRepo.branches());
}
}
@Test
void mirrorSelectedBranchPattern(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var gitConfig = toDir.resolve(".git").resolve("config");
Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var newFile = fromDir.resolve("this-file-cannot-exist.txt");
Files.writeString(newFile, "Hello world\n");
fromLocalRepo.add(newFile);
var first = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var featureBranch = fromLocalRepo.branch(first, "feature");
fromLocalRepo.checkout(featureBranch, false);
assertEquals(Optional.of(featureBranch), fromLocalRepo.currentBranch());
Files.writeString(newFile, "Hello again\n", StandardOpenOption.APPEND);
fromLocalRepo.add(newFile);
var second = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
assertEquals(Optional.of(first), fromLocalRepo.resolve("master"));
assertEquals(Optional.of(second), fromLocalRepo.resolve("feature"));
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(2, fromCommits.size());
var toCommits = toLocalRepo.commits().asList();
assertEquals(0, toCommits.size());
var storage = temp.path().resolve("storage");
var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(Pattern.compile("f.*")), false, false, List.of());
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(2, toCommits.size());
assertEquals(second, toCommits.get(0).hash());
assertEquals(List.of(new Branch("feature")), toLocalRepo.branches());
}
}
@Test
void mirrorMasterBranchWithExistingCloneDirectory(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var gitConfig = toDir.resolve(".git").resolve("config");
Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var newFile = fromDir.resolve("this-file-cannot-exist.txt");
Files.writeString(newFile, "Hello world\n");
fromLocalRepo.add(newFile);
var newHash = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(newHash, fromCommits.get(0).hash());
var toCommits = toLocalRepo.commits().asList();
assertEquals(0, toCommits.size());
var storage = temp.path().resolve("storage");
var sanitizedUrl =
URLEncoder.encode(toHostedRepo.webUrl().toString(), StandardCharsets.UTF_8);
var temporaryDir = storage.resolve(sanitizedUrl);
Files.createDirectories(temporaryDir);
var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo);
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(newHash, toCommits.get(0).hash());
}
}
/**
* Tests mirroring only tags
*/
@Test
void mirrorOnlyTags(TestInfo testInfo) throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var gitConfig = toDir.resolve(".git").resolve("config");
Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var newFile = fromDir.resolve("this-file-cannot-exist.txt");
Files.writeString(newFile, "Hello world\n");
fromLocalRepo.add(newFile);
var newHash = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(newHash, fromCommits.get(0).hash());
fromLocalRepo.tag(newHash, "first", "add first tag", "duke", "duk@openjdk.org");
fromLocalRepo.tag(newHash, "second", "add second tag", "duke", "duk@openjdk.org");
var toCommits = toLocalRepo.commits().asList();
assertEquals(0, toCommits.size());
assertEquals(0, toLocalRepo.tags().size());
assertEquals(0, toLocalRepo.branches().size());
var storage = temp.path().resolve("storage");
var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(), true, true, List.of());
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(newHash, toCommits.get(0).hash());
var toTags = toLocalRepo.tags();
assertEquals(2, toTags.size());
assertTrue(toTags.contains(new Tag("first")));
assertTrue(toTags.contains(new Tag("second")));
assertEquals(0, toLocalRepo.branches().size());
// Add another tag and go again
fromLocalRepo.tag(newHash, "third", "add third tag", "duke", "duk@openjdk.org");
TestBotRunner.runPeriodicItems(bot);
toTags = toLocalRepo.tags();
assertEquals(3, toTags.size());
assertTrue(toTags.contains(new Tag("first")));
assertTrue(toTags.contains(new Tag("second")));
assertTrue(toTags.contains(new Tag("third")));
assertEquals(0, toLocalRepo.branches().size());
toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
// Change a tag and go again
Files.writeString(newFile, "Hello world again\n", StandardOpenOption.APPEND);
fromLocalRepo.add(newFile);
var secondHash = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var firstTag = fromLocalRepo.tag(secondHash, "first", "add first tag again", "duke", "duk@openjdk.org", null, true);
TestBotRunner.runPeriodicItems(bot);
assertEquals(0, toLocalRepo.branches().size());
toCommits = toLocalRepo.commits().asList();
assertEquals(2, toCommits.size());
toTags = toLocalRepo.tags();
assertEquals(3, toTags.size());
assertEquals(fromLocalRepo.annotate(firstTag), toLocalRepo.annotate(firstTag), "First tag not correctly mirrored");
}
}
@Test
void mirrorRefspecs() throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var gitConfig = toDir.resolve(".git").resolve("config");
Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var newFile = fromDir.resolve("this-file-cannot-exist.txt");
Files.writeString(newFile, "Hello world\n");
fromLocalRepo.add(newFile);
var newHash = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(1, fromCommits.size());
assertEquals(newHash, fromCommits.get(0).hash());
var toCommits = toLocalRepo.commits().asList();
assertEquals(0, toCommits.size());
var storage = temp.path().resolve("storage");
var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(), false, false,
List.of("refs/heads/master:refs/heads/master"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(1, toCommits.size());
assertEquals(newHash, toCommits.get(0).hash());
}
}
@Test
void mirrorMultipleRefspecs() throws IOException {
try (var temp = new TemporaryDirectory()) {
var host = TestHost.createNew(List.of(HostUser.create(0, "duke", "J. Duke")));
var fromDir = temp.path().resolve("from.git");
var fromLocalRepo = TestableRepository.init(fromDir, VCS.GIT);
var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
var toDir = temp.path().resolve("to.git");
var toLocalRepo = TestableRepository.init(toDir, VCS.GIT);
var gitConfig = toDir.resolve(".git").resolve("config");
Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
StandardOpenOption.APPEND);
var toHostedRepo = new TestHostedRepository(host, "test-mirror", toLocalRepo);
var newFile = fromDir.resolve("this-file-cannot-exist.txt");
Files.writeString(newFile, "Hello world\n");
fromLocalRepo.add(newFile);
var first = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
var featureBranch = fromLocalRepo.branch(first, "feature");
fromLocalRepo.checkout(featureBranch, false);
assertEquals(Optional.of(featureBranch), fromLocalRepo.currentBranch());
Files.writeString(newFile, "Hello again\n", StandardOpenOption.APPEND);
fromLocalRepo.add(newFile);
var second = fromLocalRepo.commit("An additional commit", "duke", "duke@openjdk.org");
assertEquals(Optional.of(first), fromLocalRepo.resolve("master"));
assertEquals(Optional.of(second), fromLocalRepo.resolve("feature"));
fromLocalRepo.tag(first, "firstTag", "add first tag", "duke", "duk@openjdk.org");
fromLocalRepo.tag(second, "secondTag", "add second tag", "duke", "duk@openjdk.org");
var fromCommits = fromLocalRepo.commits().asList();
assertEquals(2, fromCommits.size());
var toCommits = toLocalRepo.commits().asList();
assertEquals(0, toCommits.size());
var storage = temp.path().resolve("storage");
var bot = new MirrorBot(storage, fromHostedRepo, toHostedRepo, List.of(), false, false,
List.of("refs/heads/m*:refs/heads/m*", "refs/tags/s*:refs/tags/s*"));
TestBotRunner.runPeriodicItems(bot);
toCommits = toLocalRepo.commits().asList();
assertEquals(2, toCommits.size());
assertEquals(second, toCommits.get(0).hash());
assertEquals(List.of(new Branch("master")), toLocalRepo.branches());
assertEquals(List.of(new Tag("secondTag")), toLocalRepo.tags());
}
}
}
================================================
FILE: bots/mlbridge/build.gradle
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module {
name = 'org.openjdk.skara.bots.mlbridge'
test {
requires 'org.junit.jupiter.api'
requires 'org.openjdk.skara.test'
opens 'org.openjdk.skara.bots.mlbridge' to 'org.junit.platform.commons'
}
}
dependencies {
implementation project(':ci')
implementation project(':bot')
implementation project(':mailinglist')
implementation project(':host')
implementation project(':forge')
implementation project(':issuetracker')
implementation project(':network')
implementation project(':census')
implementation project(':vcs')
implementation project(':jcheck')
implementation project(':json')
implementation project(':email')
implementation project(':webrev')
implementation project(':version')
implementation project(':metrics')
implementation project(':bots:common')
implementation project(':jbs')
testImplementation project(':test')
}
================================================
FILE: bots/mlbridge/src/main/java/module-info.java
================================================
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
module org.openjdk.skara.bots.mlbridge {
requires org.openjdk.skara.bot;
requires org.openjdk.skara.mailinglist;
requires org.openjdk.skara.host;
requires org.openjdk.skara.census;
requires org.openjdk.skara.vcs;
requires org.openjdk.skara.jcheck;
requires org.openjdk.skara.json;
requires org.openjdk.skara.email;
requires org.openjdk.skara.webrev;
requires org.openjdk.skara.network;
requires org.openjdk.skara.version;
requires org.openjdk.skara.jbs;
requires org.openjdk.skara.bots.common;
requires java.logging;
requires java.net.http;
provides org.openjdk.skara.bot.BotFactory with org.openjdk.skara.bots.mlbridge.MailingListBridgeBotFactory;
}
================================================
FILE: bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveItem.java
================================================
/*
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.mlbridge;
import org.openjdk.skara.forge.*;
import org.openjdk.skara.host.HostUser;
import org.openjdk.skara.issuetracker.Comment;
import org.openjdk.skara.vcs.*;
import java.io.*;
import java.net.URI;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
class ArchiveItem {
private final String id;
private final ZonedDateTime created;
private final ZonedDateTime updated;
private final HostUser author;
private final Map extraHeaders;
private final ArchiveItem parent;
private final Supplier subject;
private final Supplier header;
private String resolvedHeader;
private final Supplier body;
private String resolvedBody;
private final Supplier footer;
private String resolvedFooter;
private ArchiveItem(ArchiveItem parent, String id, ZonedDateTime created, ZonedDateTime updated, HostUser author, Map extraHeaders, Supplier subject, Supplier header, Supplier body, Supplier footer) {
this.id = id;
this.created = created;
this.updated = updated;
this.author = author;
this.extraHeaders = extraHeaders;
this.parent = parent;
this.subject = subject;
this.header = header;
this.body = body;
this.footer = footer;
}
private static Optional mergeCommit(PullRequest pr, Repository localRepo, Hash head) {
try {
var author = new Author("duke", "duke@openjdk.org");
var hash = PullRequestUtils.createCommit(pr, localRepo, head, author, author, pr.title());
return localRepo.lookup(hash);
} catch (IOException | CommitFailure e) {
return Optional.empty();
}
}
private static Optional conflictCommit(PullRequest pr, Repository localRepo, Hash head) {
try {
localRepo.checkout(head, true);
} catch (IOException e) {
return Optional.empty();
}
try {
localRepo.merge(PullRequestUtils.targetHash(localRepo));
// No problem means no conflict
return Optional.empty();
} catch (IOException e) {
try {
var status = localRepo.status();
var unmerged = status.stream()
.filter(entry -> entry.status().isUnmerged())
.map(entry -> entry.source().path())
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
// Drop the successful merges from the stage
localRepo.reset(head, false);
// Add the unmerged files as-is (retaining the conflict markers)
localRepo.add(unmerged);
var hash = localRepo.commit("Conflicts in " + pr.title(), "duke", "duke@openjdk.org");
localRepo.clean();
return localRepo.lookup(hash);
} catch (IOException ioException) {
return Optional.empty();
}
}
}
static ArchiveItem from(PullRequest pr, Repository localRepo, HostUserToEmailAuthor hostUserToEmailAuthor,
URI issueTracker, String issuePrefix, WebrevStorage.WebrevGenerator webrevGenerator,
WebrevNotification webrevNotification, ZonedDateTime created, ZonedDateTime updated,
Hash base, Hash head, String subjectPrefix, String threadPrefix) {
return new ArchiveItem(null, "fc", created, updated, pr.author(), Map.of("PR-Head-Hash", head.hex(),
"PR-Base-Hash", base.hex(),
"PR-Thread-Prefix", threadPrefix),
() -> subjectPrefix + threadPrefix + (threadPrefix.isEmpty() ? "" : ": ") + pr.title(),
() -> "",
() -> ArchiveMessages.composeConversation(pr),
() -> {
if (PullRequestUtils.isMerge(pr)) {
var mergeWebrevs = new ArrayList();
var conflictCommit = conflictCommit(pr, localRepo, head);
conflictCommit.ifPresent(commit -> mergeWebrevs.add(
webrevGenerator.generate(commit.parentDiffs().get(0), "00.conflicts", WebrevDescription.Type.MERGE_CONFLICT, pr.targetRef())));
var mergeCommit = mergeCommit(pr, localRepo, head);
if (mergeCommit.isPresent()) {
for (int i = 0; i < mergeCommit.get().parentDiffs().size(); ++i) {
var diff = mergeCommit.get().parentDiffs().get(i);
if (diff.patches().size() == 0) {
continue;
}
switch (i) {
case 0:
mergeWebrevs.add(webrevGenerator.generate(diff, String.format("00.%d", i), WebrevDescription.Type.MERGE_TARGET, pr.targetRef()));
break;
case 1:
var mergeSource = pr.title().length() > 6 ? pr.title().substring(6) : null;
mergeWebrevs.add(webrevGenerator.generate(diff, String.format("00.%d", i), WebrevDescription.Type.MERGE_SOURCE, mergeSource));
break;
default:
mergeWebrevs.add(webrevGenerator.generate(diff, String.format("00.%d", i), WebrevDescription.Type.MERGE_SOURCE, null));
break;
}
}
if (!mergeWebrevs.isEmpty()) {
webrevNotification.notify(0, mergeWebrevs);
}
}
return ArchiveMessages.composeMergeConversationFooter(pr, localRepo, mergeWebrevs, base, head);
} else {
var fullWebrev = webrevGenerator.generate(base, head, "00", WebrevDescription.Type.FULL);
webrevNotification.notify(0, List.of(fullWebrev));
return ArchiveMessages.composeConversationFooter(pr, issueTracker, issuePrefix, localRepo, fullWebrev, base, head);
}
});
}
private static Optional rebasedLastHead(Repository localRepo, Hash newBase, Hash lastHead) {
try {
localRepo.checkout(lastHead, true);
localRepo.rebase(newBase, "duke", "duke@openjdk.org");
var rebasedLastHead = localRepo.head();
return Optional.of(rebasedLastHead);
} catch (IOException e) {
return Optional.empty();
}
}
/**
* Checks if lastHead is available in the local repository and tried to fetch it
* if not.
*/
private static boolean lastHeadAvailable(PullRequest pr, Repository localRepo, Hash lastHead, boolean tryFetch) {
try {
if (localRepo.resolve(lastHead.hex()).isPresent()) {
return true;
}
if (tryFetch) {
return localRepo.fetch(pr.repository().authenticatedUrl(), lastHead.hex(), false).isPresent();
}
} catch (IOException e) {
return false;
}
return false;
}
private static String hostUserToCommitterName(HostUserToEmailAuthor hostUserToEmailAuthor, HostUser hostUser) {
var email = hostUserToEmailAuthor.author(hostUser);
if (email.fullName().isPresent()) {
return email.fullName().get();
} else {
return hostUser.fullName();
}
}
static ArchiveItem from(PullRequest pr, Repository localRepo, HostUserToEmailAuthor hostUserToEmailAuthor,
WebrevStorage.WebrevGenerator webrevGenerator, WebrevNotification webrevNotification,
ZonedDateTime created, ZonedDateTime updated, Hash lastBase, Hash lastHead, Hash base,
Hash head, int index, ArchiveItem parent, String subjectPrefix, String threadPrefix) {
return new ArchiveItem(parent, "ha" + head.hex(), created, updated, pr.author(), Map.of("PR-Head-Hash", head.hex(), "PR-Base-Hash", base.hex()),
() -> String.format("Re: %s%s%s [v%d]", subjectPrefix, threadPrefix + (threadPrefix.isEmpty() ? "" : ": "), pr.title(), index + 1),
() -> "",
() -> {
if (lastBase.equals(base)) {
// Make sure lastHead is present in the local repo (if possible)
lastHeadAvailable(pr, localRepo, lastHead, true);
return ArchiveMessages.composeIncrementalRevision(pr, localRepo, hostUserToCommitterName(hostUserToEmailAuthor, pr.author()), head, lastHead, base);
} else {
var rebasedLastHead = rebasedLastHead(localRepo, base, lastHead);
if (rebasedLastHead.isPresent()) {
return ArchiveMessages.composeRebasedIncrementalRevision(pr, localRepo, hostUserToCommitterName(hostUserToEmailAuthor, pr.author()), head, rebasedLastHead.get());
} else {
return ArchiveMessages.composeFullRevision(pr, localRepo, hostUserToCommitterName(hostUserToEmailAuthor, pr.author()), base, head);
}
}
},
() -> {
var fullWebrev = webrevGenerator.generate(base, head, String.format("%02d", index), WebrevDescription.Type.FULL);
if (lastBase.equals(base)) {
if (lastHeadAvailable(pr, localRepo, lastHead, false)) {
var incrementalWebrev = webrevGenerator.generate(lastHead, head, String.format("%02d-%02d", index - 1, index), WebrevDescription.Type.INCREMENTAL);
webrevNotification.notify(index, List.of(fullWebrev, incrementalWebrev));
return ArchiveMessages.composeIncrementalFooter(pr, localRepo, fullWebrev, incrementalWebrev, head, lastHead);
} else {
webrevNotification.notify(index, List.of(fullWebrev));
return ArchiveMessages.composeRebasedFooter(pr, localRepo, fullWebrev, base, head);
}
} else {
var rebasedLastHead = rebasedLastHead(localRepo, base, lastHead);
if (rebasedLastHead.isPresent()) {
var incrementalWebrev = webrevGenerator.generate(rebasedLastHead.get(), head, String.format("%02d-%02d", index - 1, index), WebrevDescription.Type.INCREMENTAL);
webrevNotification.notify(index, List.of(fullWebrev, incrementalWebrev));
return ArchiveMessages.composeIncrementalFooter(pr, localRepo, fullWebrev, incrementalWebrev, head, lastHead);
} else {
webrevNotification.notify(index, List.of(fullWebrev));
return ArchiveMessages.composeRebasedFooter(pr, localRepo, fullWebrev, base, head);
}
}
});
}
static ArchiveItem from(PullRequest pr, Comment comment, HostUserToEmailAuthor hostUserToEmailAuthor, ArchiveItem parent) {
return new ArchiveItem(parent, "pc" + comment.id(), comment.createdAt(), comment.updatedAt(), comment.author(), Map.of(),
() -> ArchiveMessages.composeReplySubject(parent.subject()),
() -> ArchiveMessages.composeReplyHeader(parent.createdAt(), hostUserToEmailAuthor.author(parent.author)),
() -> ArchiveMessages.composeComment(comment),
() -> ArchiveMessages.composeCommentReplyFooter(pr, comment));
}
static ArchiveItem from(PullRequest pr, Review review, HostUserToEmailAuthor hostUserToEmailAuthor, HostUserToUsername hostUserToUsername, HostUserToRole hostUserToRole, ArchiveItem parent) {
return new ArchiveItem(parent, "rv" + review.id(), review.createdAt(), review.createdAt(), review.reviewer(), Map.of(),
() -> ArchiveMessages.composeReplySubject(parent.subject()),
() -> ArchiveMessages.composeReplyHeader(parent.createdAt(), hostUserToEmailAuthor.author(parent.author())),
() -> ArchiveMessages.composeReview(pr, review, hostUserToUsername, hostUserToRole),
() -> ArchiveMessages.composeReviewFooter(pr, review, hostUserToUsername, hostUserToRole));
}
static ArchiveItem from(PullRequest pr, ReviewComment reviewComment, HostUserToEmailAuthor hostUserToEmailAuthor, ArchiveItem parent) {
return new ArchiveItem(parent, "rc" + reviewComment.id(), reviewComment.createdAt(), reviewComment.updatedAt(), reviewComment.author(), Map.of(),
() -> ArchiveMessages.composeReplySubject(parent.subject()),
() -> ArchiveMessages.composeReplyHeader(parent.createdAt(), hostUserToEmailAuthor.author(parent.author())),
() -> ArchiveMessages.composeReviewComment(pr, reviewComment),
() -> ArchiveMessages.composeReviewCommentReplyFooter(pr, reviewComment));
}
static ArchiveItem closedNotice(PullRequest pr, HostUserToEmailAuthor hostUserToEmailAuthor, ArchiveItem parent, String subjectPrefix) {
var closedBy = pr.closedBy().orElse(pr.author());
return new ArchiveItem(parent, "cn", pr.updatedAt(), pr.updatedAt(), closedBy, Map.of("PR-Closed-Notice", "0"),
() -> String.format("%sWithdrawn: %s", subjectPrefix, pr.title()),
() -> ArchiveMessages.composeReplyHeader(parent.createdAt(), hostUserToEmailAuthor.author(parent.author())),
() -> ArchiveMessages.composeClosedNotice(pr),
() -> ArchiveMessages.composeReplyFooter(pr));
}
static ArchiveItem integratedNotice(PullRequest pr, Repository localRepo, Commit commit, HostUserToEmailAuthor hostUserToEmailAuthor, ArchiveItem parent, String subjectPrefix) {
return new ArchiveItem(parent, "in", pr.updatedAt(), pr.updatedAt(), pr.author(), Map.of("PR-Integrated-Notice", "0"),
() -> String.format("%sIntegrated: %s", subjectPrefix, pr.title()),
() -> ArchiveMessages.composeReplyHeader(parent.createdAt(), hostUserToEmailAuthor.author(parent.author())),
() -> ArchiveMessages.composeIntegratedNotice(pr, localRepo, commit),
() -> ArchiveMessages.composeReplyFooter(pr));
}
private static final Pattern mentionPattern = Pattern.compile("@([\\w-]+)");
private static Optional findLastMention(String commentText, List eligibleParents) {
var firstLine = commentText.lines().findFirst();
if (firstLine.isEmpty()) {
return Optional.empty();
}
var mentionMatcher = mentionPattern.matcher(firstLine.get());
if (mentionMatcher.find()) {
var username = mentionMatcher.group(1);
for (int i = eligibleParents.size() - 1; i >= 0; --i) {
if (eligibleParents.get(i).author.username().equals(username)) {
return Optional.of(eligibleParents.get(i));
}
}
}
return Optional.empty();
}
static boolean containsQuote(String quote, String body) {
var compactQuote = quote.lines()
.map(String::strip)
.filter(line -> !line.isBlank())
.takeWhile(line -> line.startsWith(">"))
.map(line -> line.replaceAll("\\W", ""))
.collect(Collectors.joining());
if (!compactQuote.isBlank()) {
var compactBody = body.replaceAll("\\W", "");
return compactBody.contains(compactQuote);
} else {
return false;
}
}
private static Optional findLastQuoted(String commentText, List eligibleParents) {
for (int i = eligibleParents.size() - 1; i >= 0; --i) {
if (containsQuote(commentText, eligibleParents.get(i).body())) {
return Optional.of(eligibleParents.get(i));
}
}
return Optional.empty();
}
static ArchiveItem findParent(List generated, List bridgedComments, Comment comment) {
var eligible = new ArrayList();
for (var item : generated) {
if (item.id().startsWith("pc") || item.id().startsWith("rv")) {
if (item.createdAt().isBefore(comment.createdAt())) {
eligible.add(item);
}
}
}
var lastMention = findLastMention(comment.body(), eligible);
if (lastMention.isPresent()) {
return lastMention.get();
}
// It is possible to quote a bridged comment when replying - make these eligible as well
for (var bridged : bridgedComments) {
var item = new ArchiveItem(generated.get(0), "br" + bridged.messageId().address(), bridged.created(), bridged.created(),
bridged.author(), null, generated.get(0).subject, null, bridged::body, null);
eligible.add(item);
}
var lastQuoted = findLastQuoted(comment.body(), eligible);
if (lastQuoted.isPresent()) {
return lastQuoted.get();
}
ArchiveItem lastRevisionItem = generated.get(0);
for (var item : generated) {
if (item.id().startsWith("ha")) {
if (item.createdAt().isBefore(comment.createdAt())) {
lastRevisionItem = item;
}
}
}
return lastRevisionItem;
}
private static ArchiveItem findRevisionItem(List generated, Hash hash) {
// Parent is revision update mail with the hash
ArchiveItem lastRevisionItem = generated.get(0);
// If no hash is given, that means the commit for the review/comment no longer exists.
// This means that no properly valid parent exists, but as we need to return one, just
// return the first element.
if (hash != null) {
for (var item : generated) {
if (item.id().startsWith("ha")) {
lastRevisionItem = item;
}
if (item.id().equals("ha" + hash.hex())) {
return item;
}
}
}
return lastRevisionItem;
}
static ArchiveItem findReviewCommentItem(List generated, ReviewComment reviewComment) {
for (var item : generated) {
if (item.id().equals("rc" + reviewComment.id())) {
return item;
}
}
throw new RuntimeException("Failed to find review comment");
}
static ArchiveItem findParent(List generated, Review review) {
return findRevisionItem(generated, review.hash().orElse(null));
}
static ArchiveItem findParent(List generated, List reviewComments, ReviewComment reviewComment) {
// Parent is previous in thread or the revision update mail with the hash
var threadId = reviewComment.threadId();
var reviewThread = reviewComments.stream()
.filter(comment -> comment.threadId().equals(threadId))
.collect(Collectors.toList());
ReviewComment previousComment = null;
var eligible = new ArrayList();
for (var threadComment : reviewThread) {
if (threadComment.equals(reviewComment)) {
break;
}
previousComment = threadComment;
eligible.add(findReviewCommentItem(generated, previousComment));
}
if (previousComment == null) {
return findRevisionItem(generated, reviewComment.hash().orElse(null));
} else {
var mentionedParent = findLastMention(reviewComment.body(), eligible);
if (mentionedParent.isPresent()) {
return mentionedParent.get();
} else {
return eligible.getLast();
}
}
}
String id() {
return id;
}
ZonedDateTime createdAt() {
return created;
}
ZonedDateTime updatedAt() {
return updated;
}
HostUser author() {
return author;
}
Map extraHeaders() {
return extraHeaders;
}
Optional parent() {
return Optional.ofNullable(parent);
}
String subject() {
return subject.get();
}
String header() {
if (resolvedHeader == null) {
resolvedHeader = header.get();
}
return resolvedHeader;
}
String body() {
if (resolvedBody == null) {
resolvedBody = body.get();
}
return resolvedBody;
}
String footer() {
if (resolvedFooter == null) {
resolvedFooter = footer.get();
}
return resolvedFooter;
}
@Override
public String toString() {
return "ArchiveItem From: " + author + " Body: " + body();
}
}
================================================
FILE: bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveMessages.java
================================================
/*
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package org.openjdk.skara.bots.mlbridge;
import org.openjdk.skara.email.EmailAddress;
import org.openjdk.skara.forge.*;
import org.openjdk.skara.issuetracker.Comment;
import org.openjdk.skara.network.URIBuilder;
import org.openjdk.skara.vcs.*;
import org.openjdk.skara.vcs.openjdk.Issue;
import java.io.*;
import java.net.URI;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import static org.openjdk.skara.bots.common.PatternEnum.COMMENT_PATTERN;
class ArchiveMessages {
private static final String WEBREV_UNAVAILABLE_COMMENT = "Webrev is not available because diff is too large";
private static String filterCommentsAndCommands(String body) {
var parsedBody = PullRequestBody.parse(body);
body = parsedBody.bodyText();
var commentMatcher = COMMENT_PATTERN.getPattern().matcher(body);
body = commentMatcher.replaceAll("");
body = ArchiveWorkItem.filterOutCommands(body);
body = MarkdownToText.removeFormatting(body);
return body.strip();
}
private static String formatCommitBrief(CommitMetadata commit) {
var ret = new StringBuilder();
var message = commit.message();
var abbrev = commit.hash().abbreviate();
if (message.size() == 0) {
ret.append(" - ").append(abbrev).append(": ");
} else {
ret.append(" - ").append(message.get(0));
}
return ret.toString();
}
private static String formatSingleCommit(CommitMetadata commit) {
var ret = new StringBuilder();
var message = commit.message();
if (message.size() == 0) {
var abbrev = commit.hash().abbreviate();
ret.append(" ").append(abbrev).append(": ");
} else {
ret.append(" ").append(String.join("\n ", message));
}
return ret.toString();
}
private static String formatCommitInList(CommitMetadata commit) {
var ret = new StringBuilder();
var message = commit.message();
if (message.size() == 0) {
var abbrev = commit.hash().abbreviate();
ret.append(" - ").append(abbrev).append(": ");
} else {
ret.append(" - ").append(String.join("\n ", message));
}
return ret.toString();
}
private static List