diff --git a/build.gradle b/build.gradle index e851468..e5d0f7b 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,9 @@ repositories { maven { url = 'https://libraries.minecraft.net' } + maven { + url = 'https://m2.chew.pro/releases' + } } java.toolchain { @@ -43,6 +46,14 @@ dependencies { implementation "com.apollographql.apollo:apollo-api:$apollo" // Apollo (GraphQL) implementation 'org.eclipse.jgit:org.eclipse.jgit:6.10.0.202406032230-r' + + implementation('org.jdbi:jdbi3-core:3.32.0') + implementation('org.jdbi:jdbi3-sqlobject:3.32.0') + implementation('org.xerial:sqlite-jdbc:3.40.0.0') + implementation('org.flywaydb:flyway-core:8.5.13') + + implementation 'net.dv8tion:JDA:5.2.2' + implementation 'pw.chew:jda-chewtils:2.0' } apollo { diff --git a/src/main/java/net/neoforged/automation/Main.java b/src/main/java/net/neoforged/automation/Main.java index f228d33..b217fba 100644 --- a/src/main/java/net/neoforged/automation/Main.java +++ b/src/main/java/net/neoforged/automation/Main.java @@ -3,6 +3,8 @@ import com.mojang.brigadier.CommandDispatcher; import io.javalin.Javalin; import net.neoforged.automation.command.Commands; +import net.neoforged.automation.db.Database; +import net.neoforged.automation.discord.DiscordBot; import net.neoforged.automation.util.AuthUtil; import net.neoforged.automation.util.GHAction; import net.neoforged.automation.webhook.handler.AutomaticLabelHandler; @@ -21,17 +23,24 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.net.http.HttpClient; import java.nio.file.Path; import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; public class Main { private static final String NEOFORGE = "neoforged/NeoForge"; public static final Logger LOGGER = LoggerFactory.getLogger("Reactionable"); + public static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); + public static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(2, Thread.ofPlatform().name("scheduled-", 0).factory()); - public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InterruptedException { var startupConfig = StartupConfiguration.load(Path.of("config.properties")); + Database.init(); + var gitHub = new GitHubBuilder() .withAuthorizationProvider(AuthUtil.githubApp( startupConfig.get("gitHubAppId", ""), @@ -51,6 +60,11 @@ public static void main(String[] args) throws IOException, NoSuchAlgorithmExcept .start(startupConfig.getInt("port", 8080)); LOGGER.warn("Started up! Logged as {} on GitHub", GitHubAccessor.getApp(gitHub).getSlug()); + + var discordToken = startupConfig.get("discordToken", ""); + if (!discordToken.isBlank()) { + DiscordBot.create(discordToken, gitHub, startupConfig, app); + } } public static WebhookHandler setupWebhookHandlers(StartupConfiguration startupConfig, WebhookHandler handler, Configuration.RepoLocation location) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { diff --git a/src/main/java/net/neoforged/automation/command/FormattingCommand.java b/src/main/java/net/neoforged/automation/command/FormattingCommand.java index 3fbb74c..b8a6fc5 100644 --- a/src/main/java/net/neoforged/automation/command/FormattingCommand.java +++ b/src/main/java/net/neoforged/automation/command/FormattingCommand.java @@ -2,7 +2,7 @@ import net.neoforged.automation.Configuration; import net.neoforged.automation.runner.PRActionRunner; -import net.neoforged.automation.runner.PRRunUtils; +import net.neoforged.automation.runner.GitRunner; import org.eclipse.jgit.transport.RefSpec; import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; import org.kohsuke.github.GHPullRequest; @@ -33,7 +33,7 @@ public static void run(GitHub gh, GHPullRequest pr, Configuration.PRActions acti .command(baseCommand, command2) .onFailed((gitHub, run) -> onFailure.accept(run)) .onFinished((gitHub, run, artifacts) -> { - PRRunUtils.setupPR(pr, (dir, git) -> { + GitRunner.setupPR(gh, pr, (dir, git, creds) -> { List deleted = new ArrayList<>(); try (var file = new ZipFile(artifacts.get("status").toFile())) { var entry = file.entries().nextElement(); @@ -69,15 +69,8 @@ public static void run(GitHub gh, GHPullRequest pr, Configuration.PRActions acti git.add().addFilepattern(".").call(); - var botName = GitHubAccessor.getApp(gitHub).getSlug() + "[bot]"; - var user = gitHub.getUser(botName); - var creds = new UsernamePasswordCredentialsProvider( - botName, - GitHubAccessor.getToken(gitHub) - ); - git.commit().setCredentialsProvider(creds) - .setCommitter(botName, user.getId() + "+" + botName + "@users.noreply.github.com") + .setCommitter(creds.getPerson()) .setMessage("Update formatting") .setSign(false) .setNoVerify(true) diff --git a/src/main/java/net/neoforged/automation/db/Database.java b/src/main/java/net/neoforged/automation/db/Database.java new file mode 100644 index 0000000..f25ffaf --- /dev/null +++ b/src/main/java/net/neoforged/automation/db/Database.java @@ -0,0 +1,61 @@ +package net.neoforged.automation.db; + +import org.flywaydb.core.Flyway; +import org.flywaydb.core.api.configuration.FluentConfiguration; +import org.jdbi.v3.core.Jdbi; +import org.jdbi.v3.core.extension.ExtensionCallback; +import org.jdbi.v3.core.extension.ExtensionConsumer; +import org.jdbi.v3.sqlobject.SqlObjectPlugin; +import org.sqlite.SQLiteDataSource; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.UnaryOperator; + +public class Database { + + private static Jdbi database; + + public static Jdbi get() { + return database; + } + + public static R withExtension(Class extensionType, ExtensionCallback callback) throws X { + return get().withExtension(extensionType, callback); + } + + public static void useExtension(Class extensionType, ExtensionConsumer callback) throws X { + get().useExtension(extensionType, callback); + } + + public static void init() { + var path = Path.of("data.db"); + database = createDatabaseConnection(path, "reactionable", c -> c.locations("classpath:db")); + } + + private static Jdbi createDatabaseConnection(Path dbPath, String name, UnaryOperator flywayConfig) { + dbPath = dbPath.toAbsolutePath(); + if (!Files.exists(dbPath)) { + try { + Files.createDirectories(dbPath.getParent()); + Files.createFile(dbPath); + } catch (IOException e) { + throw new RuntimeException("Exception creating database!", e); + } + } + final String url = "jdbc:sqlite:" + dbPath; + final SQLiteDataSource dataSource = new SQLiteDataSource(); + dataSource.setUrl(url); + dataSource.setEncoding("UTF-8"); + dataSource.setDatabaseName(name); + dataSource.setEnforceForeignKeys(true); + dataSource.setCaseSensitiveLike(false); + + final var flyway = flywayConfig.apply(Flyway.configure().dataSource(dataSource)).load(); + flyway.migrate(); + + return Jdbi.create(dataSource) + .installPlugin(new SqlObjectPlugin()); + } +} diff --git a/src/main/java/net/neoforged/automation/db/DiscordUsersDAO.java b/src/main/java/net/neoforged/automation/db/DiscordUsersDAO.java new file mode 100644 index 0000000..0693432 --- /dev/null +++ b/src/main/java/net/neoforged/automation/db/DiscordUsersDAO.java @@ -0,0 +1,16 @@ +package net.neoforged.automation.db; + +import org.jdbi.v3.sqlobject.customizer.Bind; +import org.jdbi.v3.sqlobject.statement.SqlQuery; +import org.jdbi.v3.sqlobject.statement.SqlUpdate; +import org.jdbi.v3.sqlobject.transaction.Transactional; +import org.jetbrains.annotations.Nullable; + +public interface DiscordUsersDAO extends Transactional { + @Nullable + @SqlQuery("select github from discord_users where user = :user") + String getUser(@Bind("user") long id); + + @SqlUpdate("insert into discord_users (user, github) values (:user, :github)") + void linkUser(@Bind("user") long id, @Bind("github") String githubName); +} diff --git a/src/main/java/net/neoforged/automation/discord/DiscordBot.java b/src/main/java/net/neoforged/automation/discord/DiscordBot.java new file mode 100644 index 0000000..75f6d6b --- /dev/null +++ b/src/main/java/net/neoforged/automation/discord/DiscordBot.java @@ -0,0 +1,104 @@ +package net.neoforged.automation.discord; + +import com.jagrosh.jdautilities.command.CommandClient; +import com.jagrosh.jdautilities.command.CommandClientBuilder; +import io.javalin.Javalin; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; +import net.dv8tion.jda.api.interactions.commands.Command; +import net.dv8tion.jda.api.interactions.commands.OptionMapping; +import net.neoforged.automation.Main; +import net.neoforged.automation.StartupConfiguration; +import net.neoforged.automation.discord.command.GitHubCommand; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class DiscordBot { + public static final Logger LOGGER = LoggerFactory.getLogger(DiscordBot.class); + + private static volatile List knownRepositories = List.of(); + + public static void create(String token, GitHub gitHub, StartupConfiguration configuration, Javalin app) throws InterruptedException { + var cfg = new OauthConfig(configuration.get("githubOauthClientId", ""), configuration.get("githubOauthClientSecret", ""), + configuration.get("serverUrl", "") + "/oauth2/discord/github"); + + var jda = JDABuilder.createLight(token) + .addEventListeners(createClient(gitHub, cfg)) + .build() + .awaitReady(); + + app.get("/oauth2/discord/github", new GitHubOauthLinkHandler(cfg)); + + LOGGER.info("Discord bot started. Logged in as {}", jda.getSelfUser().getEffectiveName()); + + Main.EXECUTOR.scheduleAtFixedRate(() -> updateRepos(gitHub), 0, 1, TimeUnit.HOURS); + } + + private static CommandClient createClient(GitHub gitHub, OauthConfig config) { + var builder = new CommandClientBuilder(); + builder.setOwnerId("0"); + builder.addSlashCommand(new GHLinkCommand(config.clientId, config.redirectUrl)); + builder.addSlashCommand(new GitHubCommand(gitHub)); + return builder.build(); + } + + private static void updateRepos(GitHub gitHub) { + try { + synchronized (DiscordBot.class) { + knownRepositories = gitHub.getInstallation().listRepositories().toList() + .stream() + .filter(r -> !r.isArchived() && !r.isPrivate()) + .map(GHRepository::getFullName) + .sorted() + .toList(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static void suggestRepositories(CommandAutoCompleteInteractionEvent event) { + Predicate filter = filterContainsCurrent(event); + event.replyChoices(knownRepositories.stream().filter(filter) + .limit(25) + .map(o -> new Command.Choice(o, o)) + .toList()) + .queue(); + } + + public static Consumer suggestBranches(GitHub gitHub) { + return event -> { + var repoName = event.getOption("repo", OptionMapping::getAsString); + if (repoName == null || repoName.isBlank()) return; + + try { + var repo = gitHub.getRepository(repoName); + + event.replyChoices(repo.getBranches().keySet() + .stream().filter(filterContainsCurrent(event)) + .limit(25) + .map(o -> new Command.Choice(o, o)) + .toList()).queue(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }; + } + + private static Predicate filterContainsCurrent(CommandAutoCompleteInteractionEvent event) { + var current = event.getFocusedOption().getValue().toLowerCase(Locale.ROOT); + return current.isBlank() ? e -> true : e -> e.toLowerCase(Locale.ROOT).contains(current); + } + + record OauthConfig(String clientId, String clientSecret, String redirectUrl) { + } +} diff --git a/src/main/java/net/neoforged/automation/discord/GHLinkCommand.java b/src/main/java/net/neoforged/automation/discord/GHLinkCommand.java new file mode 100644 index 0000000..e6a4280 --- /dev/null +++ b/src/main/java/net/neoforged/automation/discord/GHLinkCommand.java @@ -0,0 +1,27 @@ +package net.neoforged.automation.discord; + +import com.jagrosh.jdautilities.command.SlashCommand; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.components.buttons.Button; + +public class GHLinkCommand extends SlashCommand { + private final String clientId; + private final String redirectUrl; + public GHLinkCommand(String clientId, String redirectUrl) { + this.name = "ghlink"; + this.help = "Link your Discord account to your GitHub account"; + + this.clientId = clientId; + this.redirectUrl = redirectUrl; + } + + @Override + protected void execute(SlashCommandEvent event) { + var url = "https://github.com/login/oauth/authorize?client_id=" + clientId + "&response_type=code&scope=user:read&redirect_uri=" + redirectUrl + "&state=" + event.getUser().getId(); + + event.reply("Please use the button below to link this Discord account to a GitHub one.") + .setEphemeral(true) + .addActionRow(Button.link(url, "Link account")) + .queue(); + } +} diff --git a/src/main/java/net/neoforged/automation/discord/GitHubOauthLinkHandler.java b/src/main/java/net/neoforged/automation/discord/GitHubOauthLinkHandler.java new file mode 100644 index 0000000..e8f1f76 --- /dev/null +++ b/src/main/java/net/neoforged/automation/discord/GitHubOauthLinkHandler.java @@ -0,0 +1,63 @@ +package net.neoforged.automation.discord; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.javalin.http.Context; +import io.javalin.http.Handler; +import io.javalin.http.HttpStatus; +import net.neoforged.automation.Main; +import net.neoforged.automation.db.Database; +import net.neoforged.automation.db.DiscordUsersDAO; +import org.jetbrains.annotations.NotNull; +import org.kohsuke.github.GitHubBuilder; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public record GitHubOauthLinkHandler(DiscordBot.OauthConfig config) implements Handler { + @Override + public void handle(@NotNull Context ctx) throws Exception { + var code = ctx.queryParam("code"); + var state = ctx.queryParam("state"); + if (code == null || code.isBlank() || state == null || state.isBlank()) { + ctx.result("Wrong input provided") + .status(HttpStatus.BAD_REQUEST); + return; + } + + var mapper = new ObjectMapper(); + var node = mapper.createObjectNode(); + node.put("client_id", config.clientId()); + node.put("client_secret", config.clientSecret()); + node.put("grant_type", "authorization_code"); + node.put("redirect_uri", config.redirectUrl()); + node.put("scope", "user:read"); + node.put("code", code); + + var req = Main.HTTP_CLIENT.send(HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(node.toString())) + .uri(URI.create("https://github.com/login/oauth/access_token")) + .header("Content-Type", "application/json") + .header("Accept", "application/json") + .build(), HttpResponse.BodyHandlers.ofString()); + + var asJson = mapper.readValue(req.body(), JsonNode.class); + var token = asJson.get("access_token"); + if (token == null) { + ctx.result("Error: " + req.body()) + .status(HttpStatus.BAD_REQUEST); + return; + } + + var user = new GitHubBuilder() + .withJwtToken(token.asText()) + .build() + .getMyself() + .getLogin(); + + Database.useExtension(DiscordUsersDAO.class, db -> db.linkUser(Long.parseLong(state), user)); + + ctx.result("Authenticated as " + user + ". You may close this page.").status(HttpStatus.OK); + } +} diff --git a/src/main/java/net/neoforged/automation/discord/command/BaseDiscordCommand.java b/src/main/java/net/neoforged/automation/discord/command/BaseDiscordCommand.java new file mode 100644 index 0000000..1f20429 --- /dev/null +++ b/src/main/java/net/neoforged/automation/discord/command/BaseDiscordCommand.java @@ -0,0 +1,24 @@ +package net.neoforged.automation.discord.command; + +import com.jagrosh.jdautilities.command.SlashCommand; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +public abstract class BaseDiscordCommand extends SlashCommand { + private final Map> autoComplete = new HashMap<>(); + + protected void addAutoCompleteHandler(String option, Consumer consumer) { + autoComplete.put(option, consumer); + } + + @Override + public void onAutoComplete(CommandAutoCompleteInteractionEvent event) { + var option = autoComplete.get(event.getFocusedOption().getName()); + if (option != null) { + option.accept(event); + } + } +} diff --git a/src/main/java/net/neoforged/automation/discord/command/GitHubCommand.java b/src/main/java/net/neoforged/automation/discord/command/GitHubCommand.java new file mode 100644 index 0000000..9db1e76 --- /dev/null +++ b/src/main/java/net/neoforged/automation/discord/command/GitHubCommand.java @@ -0,0 +1,90 @@ +package net.neoforged.automation.discord.command; + +import com.jagrosh.jdautilities.command.SlashCommand; +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.OptionData; +import net.neoforged.automation.discord.DiscordBot; +import net.neoforged.automation.runner.GitRunner; +import org.eclipse.jgit.transport.RefSpec; +import org.kohsuke.github.GitHub; + +import java.util.List; + +public class GitHubCommand extends BaseDiscordCommand { + public GitHubCommand(GitHub gitHub) { + this.name = "github"; + this.help = "GitHub automation commands"; + + this.children = new SlashCommand[] { + new PushTag(gitHub) + }; + } + + @Override + protected void execute(SlashCommandEvent event) { + + } + + private static class PushTag extends GitHubLinkedCommand { + + private PushTag(GitHub gitHub) { + super(gitHub); + this.name = "push-tag"; + this.help = "Push a tagged commit to a repo"; + + this.options = List.of( + new OptionData(OptionType.STRING, "repo", "The repository to push to", true).setAutoComplete(true), + new OptionData(OptionType.STRING, "tag", "The tag to push", true), + new OptionData(OptionType.STRING, "message", "The commit message", true), + new OptionData(OptionType.STRING, "branch", "The branch to push to", false).setAutoComplete(true) + ); + + addAutoCompleteHandler("repo", DiscordBot::suggestRepositories); + addAutoCompleteHandler("branch", DiscordBot.suggestBranches(gitHub)); + } + + @Override + protected void execute(SlashCommandEvent event, String githubUser) throws Exception { + var repo = gitHub.getRepository(event.optString("repo")); + + checkUserAccess(repo, githubUser); + + var tag = event.optString("tag"); + var branch = event.optString("branch", repo.getDefaultBranch()); + + event.deferReply().queue(); + + GitRunner.setupRepo( + gitHub, + repo, + branch, + (dir, git, creds) -> { + git.commit() + .setCredentialsProvider(creds) + .setCommitter(creds.getPerson()) + .setMessage(event.optString("message")) + .setSign(false) + .setNoVerify(true) + .call(); + + git.tag() + .setCredentialsProvider(creds) + .setSigned(false) + .setName(tag) + .setTagger(creds.getPerson()) + .setAnnotated(true) + .call(); + + git.push() + .setCredentialsProvider(creds) + .setRemote("origin") + .setRefSpecs(new RefSpec(branch), new RefSpec(tag)) + .call(); + } + ); + + event.getHook().sendMessage("Pushed tag `" + tag + "` to branch `" + branch + "`.").queue(); + } + } +} diff --git a/src/main/java/net/neoforged/automation/discord/command/GitHubLinkedCommand.java b/src/main/java/net/neoforged/automation/discord/command/GitHubLinkedCommand.java new file mode 100644 index 0000000..b8bef88 --- /dev/null +++ b/src/main/java/net/neoforged/automation/discord/command/GitHubLinkedCommand.java @@ -0,0 +1,46 @@ +package net.neoforged.automation.discord.command; + +import com.jagrosh.jdautilities.command.SlashCommandEvent; +import net.neoforged.automation.db.Database; +import net.neoforged.automation.db.DiscordUsersDAO; +import org.kohsuke.github.GHPermissionType; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; + +public abstract class GitHubLinkedCommand extends BaseDiscordCommand { + protected final GitHub gitHub; + + protected GitHubLinkedCommand(GitHub gitHub) { + this.gitHub = gitHub; + } + + @Override + protected void execute(SlashCommandEvent event) { + var github = Database.withExtension(DiscordUsersDAO.class, db -> db.getUser(event.getUser().getIdLong())); + if (github == null) { + event.reply("Please link your GitHub account first.").queue(); + return; + } + try { + execute(event, github); + } catch (ValidationException v) { + event.getHook().sendMessage(v.getMessage()).queue(); + } catch (Exception ex) { + event.getHook().sendMessage("Failed: " + ex.getMessage()).queue(); + } + } + + protected abstract void execute(SlashCommandEvent event, String githubUser) throws Exception; + + protected void checkUserAccess(GHRepository repo, String githubUser) throws Exception { + if (!repo.hasPermission(githubUser, GHPermissionType.WRITE)) { + throw new ValidationException("You cannot push to this repo!"); + } + } + + protected static class ValidationException extends Exception { + public ValidationException(String msg) { + super(msg); + } + } +} diff --git a/src/main/java/net/neoforged/automation/runner/GitRunner.java b/src/main/java/net/neoforged/automation/runner/GitRunner.java new file mode 100644 index 0000000..99aaec0 --- /dev/null +++ b/src/main/java/net/neoforged/automation/runner/GitRunner.java @@ -0,0 +1,82 @@ +package net.neoforged.automation.runner; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import org.eclipse.jgit.util.FileUtils; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHUser; +import org.kohsuke.github.GitHub; +import org.kohsuke.github.GitHubAccessor; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Random; + +public class GitRunner { + private static final Random RANDOM = new Random(); + + public static void setupPR(GitHub gitHub, GHPullRequest pr, GitConsumer consumer) throws IOException, GitAPIException { + var repo = Path.of("checkout/prs/" + pr.getRepository().getFullName() + "/pr" + pr.getNumber() + "/" + nextInt()); + setupRepo(repo, gitHub, pr.getHead().getRepository(), pr.getHead().getRef(), consumer); + } + + public static void setupRepo(GitHub gitHub, GHRepository repo, String head, GitConsumer consumer) throws IOException, GitAPIException { + var path = Path.of("checkout/repos/" + repo.getFullName() + "/" + nextInt()); + setupRepo(path, gitHub, repo, head, consumer); + } + + public static void setupRepo(Path path, GitHub gitHub, GHRepository repo, String head, GitConsumer consumer) throws IOException, GitAPIException { + Files.createDirectories(path); + try (var git = Git.cloneRepository() + .setURI("https://github.com/" + repo.getFullName() + ".git") + .setBranch("refs/heads/" + head) + .setDirectory(path.toFile()).call()) { + var botName = GitHubAccessor.getApp(gitHub).getSlug() + "[bot]"; + var user = gitHub.getUser(botName); + var creds = new BotCredentialsProvider( + botName, + GitHubAccessor.getToken(gitHub), + user + ); + + consumer.run(path, git, creds); + + git.getRepository().close(); + } + + FileUtils.delete(path.toFile(), FileUtils.RECURSIVE); + } + + private static synchronized int nextInt() { + return RANDOM.nextInt(10000); + } + + @FunctionalInterface + public interface GitConsumer { + void run(Path dir, Git git, BotCredentialsProvider creds) throws IOException, GitAPIException; + } + + public static class BotCredentialsProvider extends UsernamePasswordCredentialsProvider { + private final GHUser bot; + public BotCredentialsProvider(String username, String password, GHUser bot) { + super(username, password); + this.bot = bot; + } + + public String getUser() { + return bot.getLogin(); + } + + public String getEmail() { + return bot.getId() + "+" + bot.getLogin() + "@users.noreply.github.com"; + } + + public PersonIdent getPerson() { + return new PersonIdent(getUser(), getEmail()); + } + } +} diff --git a/src/main/java/net/neoforged/automation/runner/PRRunUtils.java b/src/main/java/net/neoforged/automation/runner/PRRunUtils.java deleted file mode 100644 index bac32f7..0000000 --- a/src/main/java/net/neoforged/automation/runner/PRRunUtils.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.neoforged.automation.runner; - -import org.apache.commons.io.FileUtils; -import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.api.errors.GitAPIException; -import org.eclipse.jgit.transport.URIish; -import org.kohsuke.github.GHPullRequest; - -import java.io.IOException; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Random; - -public class PRRunUtils { - public static void setupPR(GHPullRequest pr, GitConsumer consumer) throws IOException, GitAPIException { - var repo = Path.of("checkedoutprs/" + pr.getRepository().getFullName() + "/pr" + pr.getNumber() + "/" + new Random().nextInt(10000)); - Files.createDirectories(repo); - try (var git = Git.init().setDirectory(repo.toFile()).call()) { - git.remoteAdd().setName("origin").setUri(new URIish("https://github.com/" + pr.getHead().getRepository().getFullName() + ".git")).call(); - git.fetch().setRemote("origin").setRefSpecs("refs/heads/" + pr.getHead().getRef() + ":" + pr.getHead().getRef()).call(); - git.checkout().setName(pr.getHead().getRef()).call(); - - consumer.run(repo, git); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - FileUtils.deleteDirectory(repo.toFile()); - } - - @FunctionalInterface - public interface GitConsumer { - void run(Path dir, Git git) throws IOException, GitAPIException; - } -} diff --git a/src/main/java/org/kohsuke/github/GitHubAccessor.java b/src/main/java/org/kohsuke/github/GitHubAccessor.java index c5bd24a..c2bd10a 100644 --- a/src/main/java/org/kohsuke/github/GitHubAccessor.java +++ b/src/main/java/org/kohsuke/github/GitHubAccessor.java @@ -97,6 +97,10 @@ public static void merge(GHPullRequest pr, String title, String message, GHPullR .send(); } + public static GHUser getAuthor(GHPullRequest pr) { + return pr.user; + } + public static String getToken(GitHub gitHub) throws IOException { return gitHub.getClient().getEncodedAuthorization().replace("Bearer ", ""); } diff --git a/src/main/resources/db/V1__oatuh.sql b/src/main/resources/db/V1__oatuh.sql new file mode 100644 index 0000000..310b05c --- /dev/null +++ b/src/main/resources/db/V1__oatuh.sql @@ -0,0 +1,5 @@ +create table discord_users +( + user big int not null primary key, + github text not null +) without rowid;