From b2a5f57a6e46dc44adc917a50af733aae0ce819f Mon Sep 17 00:00:00 2001 From: porcelli Date: Mon, 15 Oct 2018 14:29:04 -0400 Subject: [PATCH] initial bc-github integration sample application --- .gitignore | 22 +++ bc-github-githook/pom.xml | 45 +++++ .../me/git/integration/githook/GitHook.java | 119 ++++++++++++ .../integration/githook/command/Command.java | 5 + .../command/GetPreviousCommitCommand.java | 24 +++ .../githook/command/GetRepoName.java | 13 ++ .../githook/command/RefUpdateCommand.java | 82 ++++++++ .../githook/command/SetupRemote.java | 47 +++++ .../githook/command/SquashCommand.java | 94 +++++++++ .../githook/command/TrackingStatus.java | 37 ++++ .../command/TrackingStatusCommand.java | 25 +++ .../githook/github/GitHubCredentials.java | 37 ++++ .../githook/github/GitHubIntegration.java | 44 +++++ bc-github-webhook/pom.xml | 100 ++++++++++ .../integration/webhook/BCIntegration.java | 92 +++++++++ .../me/git/integration/webhook/WebHook.java | 47 +++++ .../webhook/WebHookApplication.java | 21 ++ .../webhook/json/MappingModule.java | 99 ++++++++++ .../git/integration/webhook/model/Head.java | 53 ++++++ .../integration/webhook/model/Payload.java | 146 ++++++++++++++ .../webhook/model/PullRequest.java | 180 ++++++++++++++++++ .../webhook/model/PullRequestEvent.java | 89 +++++++++ .../integration/webhook/model/PushEvent.java | 50 +++++ .../integration/webhook/model/Repository.java | 109 +++++++++++ .../git/integration/webhook/model/User.java | 34 ++++ .../webhook/resource/HookResource.java | 73 +++++++ pom.xml | 106 +++++++++++ 27 files changed, 1793 insertions(+) create mode 100644 .gitignore create mode 100644 bc-github-githook/pom.xml create mode 100644 bc-github-githook/src/main/java/porcelli/me/git/integration/githook/GitHook.java create mode 100644 bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/Command.java create mode 100644 bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/GetPreviousCommitCommand.java create mode 100644 bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/GetRepoName.java create mode 100644 bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/RefUpdateCommand.java create mode 100644 bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/SetupRemote.java create mode 100644 bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/SquashCommand.java create mode 100644 bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/TrackingStatus.java create mode 100644 bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/TrackingStatusCommand.java create mode 100644 bc-github-githook/src/main/java/porcelli/me/git/integration/githook/github/GitHubCredentials.java create mode 100644 bc-github-githook/src/main/java/porcelli/me/git/integration/githook/github/GitHubIntegration.java create mode 100644 bc-github-webhook/pom.xml create mode 100644 bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/BCIntegration.java create mode 100644 bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/WebHook.java create mode 100644 bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/WebHookApplication.java create mode 100644 bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/json/MappingModule.java create mode 100644 bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/Head.java create mode 100644 bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/Payload.java create mode 100644 bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/PullRequest.java create mode 100644 bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/PullRequestEvent.java create mode 100644 bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/PushEvent.java create mode 100644 bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/Repository.java create mode 100644 bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/User.java create mode 100644 bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/resource/HookResource.java create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b3b07cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +/target +/local +target/ +# Eclipse, Netbeans and IntelliJ files +/.* +/**/.* +!.gitignore +/nbproject +*.ipr +*.iws +*.iml + +# Repository wide ignore mac DS_Store files +.DS_Store + +# Created by Zanata +/org.uberfire +/org.dashbuilder + +# Live editing asciidoc leaves .html files behind in the source dir +uberfire-docs/src/main/asciidoc/*.html +**/dependency-reduced-pom.xml diff --git a/bc-github-githook/pom.xml b/bc-github-githook/pom.xml new file mode 100644 index 0000000..68017fb --- /dev/null +++ b/bc-github-githook/pom.xml @@ -0,0 +1,45 @@ + + + + 4.0.0 + + + me.porcelli.bc-github-integration + bc-github-integration-parent + 1.0-SNAPSHOT + ../pom.xml + + + bc-github-githook + jar + + bc-github-githook + + + + org.eclipse.jgit + org.eclipse.jgit + + + org.kohsuke + github-api + + + + + + + maven-jar-plugin + 3.1.0 + + + + porcelli.me.git.integration.githook.GitHook + + + + + + + diff --git a/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/GitHook.java b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/GitHook.java new file mode 100644 index 0000000..fdd7948 --- /dev/null +++ b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/GitHook.java @@ -0,0 +1,119 @@ +package porcelli.me.git.integration.githook; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ListBranchCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.transport.RefSpec; +import porcelli.me.git.integration.githook.command.GetPreviousCommitCommand; +import porcelli.me.git.integration.githook.command.GetRepoName; +import porcelli.me.git.integration.githook.command.SetupRemote; +import porcelli.me.git.integration.githook.command.SquashCommand; +import porcelli.me.git.integration.githook.command.TrackingStatus; +import porcelli.me.git.integration.githook.command.TrackingStatusCommand; +import porcelli.me.git.integration.githook.github.GitHubCredentials; +import porcelli.me.git.integration.githook.github.GitHubIntegration; + +import static java.util.Comparator.comparing; + +public class GitHook { + + public static void main(String[] args) throws IOException, GitAPIException { + final Path currentPath = new File("").toPath().toAbsolutePath(); + final String parentFolderName = currentPath.getParent().getName(currentPath.getParent().getNameCount() - 1).toString(); + if (parentFolderName.equalsIgnoreCase("system")) { + return; + } + final Repository repo = new FileRepositoryBuilder() + .setGitDir(currentPath.toFile()) + .build(); + + final Git git = new Git(repo); + final StoredConfig storedConfig = repo.getConfig(); + final Set remotes = storedConfig.getSubsections("remote"); + + final GitHubCredentials ghCredentials = new GitHubCredentials(); + final GitHubIntegration integration = new GitHubIntegration(ghCredentials); + + if (remotes.isEmpty()) { + new SetupRemote(ghCredentials, integration).execute(git, currentPath); + return; + } + + final List branches = git.branchList().setListMode(ListBranchCommand.ListMode.ALL).call(); + final RevWalk revWalk = new RevWalk(git.getRepository()); + + branches.stream() + .map(branch -> { + try { + return revWalk.parseCommit(branch.getObjectId()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }) + .max(comparing((RevCommit commit) -> commit.getAuthorIdent().getWhen())) + .ifPresent(newestCommit -> { + RevCommit commit = newestCommit; + try { + boolean hasSyncOnCommitMessage = commit.getFullMessage().trim().startsWith("sync:"); + + if (hasSyncOnCommitMessage) { + final Map branchesAffected = git + .nameRev() + .addPrefix("refs/heads") + .add(commit) + .call(); + + for (String remoteName : remotes) { + final String remoteURL = storedConfig.getString("remote", remoteName, "url"); + for (String ref : branchesAffected.values()) { + final String remote = storedConfig.getString("branch", ref, "remote"); + + if (remote == null) { + git.push() + .setRefSpecs(new RefSpec(ref + ":" + ref)) + .setRemote(remoteURL) + .setCredentialsProvider(ghCredentials.getCredentials()) + .call(); + storedConfig.setString("branch", ref, "remote", "origin"); + storedConfig.setString("branch", ref, "merge", "refs/heads/" + ref); + storedConfig.save(); + if (ref.contains("-pr")) { + integration.createPR(new GetRepoName().execute(currentPath), ref); + } + } else { + git.fetch().call(); + final TrackingStatusCommand trackingStatusCommand = new TrackingStatusCommand(git.getRepository()); + final TrackingStatus counts = trackingStatusCommand.getCounts(ref); + if (counts.getCommitsAhead() > 0) { + final RevCommit id = new GetPreviousCommitCommand(repo).execute(commit, counts.getCommitsAhead() - 1); + new SquashCommand(git, ref, id.name(), commit.getFullMessage()).execute(commit); + } + git.push() + .setRefSpecs(new RefSpec(ref + ":" + ref)) + .setRemote(remoteURL) + .setCredentialsProvider(ghCredentials.getCredentials()) + .call(); + } + } + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/Command.java b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/Command.java new file mode 100644 index 0000000..5e4af7a --- /dev/null +++ b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/Command.java @@ -0,0 +1,5 @@ +package porcelli.me.git.integration.githook.command; + +public interface Command { + +} diff --git a/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/GetPreviousCommitCommand.java b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/GetPreviousCommitCommand.java new file mode 100644 index 0000000..36ecf69 --- /dev/null +++ b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/GetPreviousCommitCommand.java @@ -0,0 +1,24 @@ +package porcelli.me.git.integration.githook.command; + +import java.io.IOException; + +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; + +public class GetPreviousCommitCommand implements Command { + + private final Repository repo; + + public GetPreviousCommitCommand(Repository repo) { + this.repo = repo; + } + + public RevCommit execute(final RevCommit commit, + final int commitsAhead) throws IOException { + RevCommit result = commit; + for (int i = 0; i < commitsAhead; i++) { + result = repo.parseCommit(repo.resolve(result.getParent(0).name())); + } + return result; + } +} diff --git a/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/GetRepoName.java b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/GetRepoName.java new file mode 100644 index 0000000..8ff9166 --- /dev/null +++ b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/GetRepoName.java @@ -0,0 +1,13 @@ +package porcelli.me.git.integration.githook.command; + +import java.nio.file.Path; + +public class GetRepoName implements Command { + + public String execute(final Path currentPath) { + return currentPath + .getName(currentPath.getNameCount() - 1) + .toString() + .replaceAll(".git", ""); + } +} diff --git a/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/RefUpdateCommand.java b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/RefUpdateCommand.java new file mode 100644 index 0000000..b0fc3a9 --- /dev/null +++ b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/RefUpdateCommand.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package porcelli.me.git.integration.githook.command; + +import java.io.IOException; +import java.text.MessageFormat; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.revwalk.RevCommit; + +import static java.util.Objects.requireNonNull; + +public class RefUpdateCommand implements Command { + + private final Git git; + private final String refName; + private final RevCommit commit; + + public RefUpdateCommand(final Git git, + final String refName, + final RevCommit commit) { + this.git = requireNonNull(git); + this.refName = requireNonNull(refName); + this.commit = commit; + } + + public void execute(final RevCommit lastCommit) + throws IOException, ConcurrentRefUpdateException { + requireNonNull(lastCommit, "lastCommit"); + final RefUpdate ru = git.getRepository().updateRef("refs/heads/" + refName); + if (commit == null) { + ru.setExpectedOldObjectId(ObjectId.zeroId()); + } else { + ru.setExpectedOldObjectId(commit); + } + ru.setNewObjectId(lastCommit.getId()); + ru.setRefLogMessage(commit.getFullMessage(), false); + forceUpdate(ru, commit.getId()); + } + + private void forceUpdate(final RefUpdate ru, + final ObjectId id) throws IOException, ConcurrentRefUpdateException { + final RefUpdate.Result rc = ru.forceUpdate(); + switch (rc) { + case NEW: + case FORCED: + case FAST_FORWARD: + case NO_CHANGE: + break; + case REJECTED: + case LOCK_FAILURE: + throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD, + ru.getRef(), + rc); + default: + throw new JGitInternalException(MessageFormat.format(JGitText.get().updatingRefFailed, + Constants.HEAD, + id.toString(), + rc)); + } + } +} diff --git a/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/SetupRemote.java b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/SetupRemote.java new file mode 100644 index 0000000..488d68f --- /dev/null +++ b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/SetupRemote.java @@ -0,0 +1,47 @@ +package porcelli.me.git.integration.githook.command; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; + +import porcelli.me.git.integration.githook.github.GitHubCredentials; +import porcelli.me.git.integration.githook.github.GitHubIntegration; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.ListBranchCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.StoredConfig; +import org.eclipse.jgit.transport.CredentialsProvider; + +public class SetupRemote implements Command { + + private final GitHubCredentials credentials; + private final GitHubIntegration integration; + + public SetupRemote(GitHubCredentials credentials, + GitHubIntegration integration) { + this.credentials = credentials; + this.integration = integration; + } + + public String execute(final Git git, + final Path currentPath) throws IOException, GitAPIException { + final StoredConfig storedConfig = git.getRepository().getConfig(); + + final String repoName = new GetRepoName().execute(currentPath); + final String remoteURL = integration.createRepository(repoName); + storedConfig.setString("remote", "origin", "url", remoteURL); + storedConfig.setString("remote", "origin", "fetch", "+refs/heads/*:refs/remotes/origin/*"); + + final List branches = git.branchList().setListMode(ListBranchCommand.ListMode.ALL).call(); + for (Ref value : branches) { + final String shortName = value.getName().replaceAll("refs/heads/", ""); + storedConfig.setString("branch", shortName, "remote", "origin"); + storedConfig.setString("branch", shortName, "merge", "refs/heads/" + shortName); + } + storedConfig.save(); + + git.push().setCredentialsProvider(credentials.getCredentials()).call(); + return repoName; + } +} diff --git a/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/SquashCommand.java b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/SquashCommand.java new file mode 100644 index 0000000..60f88db --- /dev/null +++ b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/SquashCommand.java @@ -0,0 +1,94 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package porcelli.me.git.integration.githook.command; + +import java.io.IOException; +import java.util.Spliterator; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; + +import static java.util.stream.StreamSupport.stream; + +public class SquashCommand implements Command { + + private final String branch; + private final Git git; + private String squashedCommitMessage; + private String startCommitString; + + public SquashCommand(final Git git, + final String branch, + final String startCommitString, + final String squashedCommitMessage) throws IOException { + this.git = git; + this.squashedCommitMessage = squashedCommitMessage; + this.branch = branch; + this.startCommitString = git.getRepository().resolve(startCommitString).name(); + } + + public void execute(final RevCommit latestCommit) throws IOException { + final Repository repo = this.git.getRepository(); + + final RevCommit startCommit = checkIfCommitIsPresentAtBranch(this.git, + this.branch, + this.startCommitString); + + RevCommit parent = startCommit; + if (startCommit.getParentCount() > 0) { + parent = startCommit.getParent(0); + } + + final CommitBuilder commitBuilder = new CommitBuilder(); + commitBuilder.setParentId(parent); + commitBuilder.setTreeId(latestCommit.getTree().getId()); + commitBuilder.setMessage(squashedCommitMessage); + commitBuilder.setAuthor(startCommit.getAuthorIdent()); + commitBuilder.setCommitter(startCommit.getAuthorIdent()); + + try (final ObjectInserter odi = repo.newObjectInserter()) { + final RevCommit squashedCommit = git.getRepository().parseCommit(odi.insert(commitBuilder)); + new RefUpdateCommand(git, branch, latestCommit).execute(squashedCommit); + } catch (ConcurrentRefUpdateException | IOException e) { + throw new RuntimeException("Error on executing squash.", e); + } + } + + private RevCommit checkIfCommitIsPresentAtBranch(final Git git, + final String branch, + final String startCommitString) throws IOException { + + try { + final ObjectId id = git.getRepository().exactRef("refs/heads/" + branch).getObjectId(); + final Spliterator log = git.log().add(id).call().spliterator(); + return stream(log, false) + .filter((elem) -> elem.getName().equals(startCommitString)) + .findFirst().orElseThrow(() -> new RuntimeException("Commit is not present at branch " + branch)); + } catch (GitAPIException | MissingObjectException | IncorrectObjectTypeException e) { + throw new RuntimeException("A problem occurred when trying to get commit list", e); + } + } +} + diff --git a/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/TrackingStatus.java b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/TrackingStatus.java new file mode 100644 index 0000000..d3004d5 --- /dev/null +++ b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/TrackingStatus.java @@ -0,0 +1,37 @@ +package porcelli.me.git.integration.githook.command; + +public class TrackingStatus { + + private final String branchName; + private final int commitsAhead; + private final int commitsBehind; + + public TrackingStatus(final String branchName) { + this(branchName, 0, 0); + } + + public TrackingStatus(final String branchName, + final int commitsAhead, + final int commitsBehind) { + this.branchName = branchName; + this.commitsAhead = commitsAhead; + this.commitsBehind = commitsBehind; + } + + public int getCommitsAhead() { + return commitsAhead; + } + + public int getCommitsBehind() { + return commitsBehind; + } + + @Override + public String toString() { + return "TrackingStatus{" + + "branchName='" + branchName + '\'' + + ", commitsAhead=" + commitsAhead + + ", commitsBehind=" + commitsBehind + + '}'; + } +} diff --git a/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/TrackingStatusCommand.java b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/TrackingStatusCommand.java new file mode 100644 index 0000000..cec6791 --- /dev/null +++ b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/command/TrackingStatusCommand.java @@ -0,0 +1,25 @@ +package porcelli.me.git.integration.githook.command; + +import java.io.IOException; + +import org.eclipse.jgit.lib.BranchTrackingStatus; +import org.eclipse.jgit.lib.Repository; + +public class TrackingStatusCommand implements Command { + + private final Repository repository; + + public TrackingStatusCommand(Repository repository) { + this.repository = repository; + } + + public TrackingStatus getCounts(final String branchName) throws IOException { + BranchTrackingStatus trackingStatus = BranchTrackingStatus.of(repository, branchName); + if (trackingStatus != null) { + return new TrackingStatus(branchName, + trackingStatus.getAheadCount(), + trackingStatus.getBehindCount()); + } + return new TrackingStatus(branchName); + } +} diff --git a/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/github/GitHubCredentials.java b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/github/GitHubCredentials.java new file mode 100644 index 0000000..6980d68 --- /dev/null +++ b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/github/GitHubCredentials.java @@ -0,0 +1,37 @@ +package porcelli.me.git.integration.githook.github; + +import java.io.File; +import java.io.FileInputStream; +import java.util.Properties; + +import org.eclipse.jgit.transport.CredentialsProvider; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; + +public class GitHubCredentials { + + private UsernamePasswordCredentialsProvider credentialsProvider = null; + private String space = null; + + public GitHubCredentials() { + File homeDir = new File(System.getProperty("user.home")); + File propertyFile = new File(homeDir, ".github"); + + Properties props = new Properties(); + try (FileInputStream in = new FileInputStream(propertyFile)) { + props.load(in); + } catch (Exception ex) { + ex.printStackTrace(); + throw new RuntimeException(ex); + } + space = props.getProperty("login"); + this.credentialsProvider = new UsernamePasswordCredentialsProvider(props.getProperty("login"), props.getProperty("password")); + } + + public CredentialsProvider getCredentials() { + return credentialsProvider; + } + + public String getSpace() { + return space; + } +} diff --git a/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/github/GitHubIntegration.java b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/github/GitHubIntegration.java new file mode 100644 index 0000000..ad99435 --- /dev/null +++ b/bc-github-githook/src/main/java/porcelli/me/git/integration/githook/github/GitHubIntegration.java @@ -0,0 +1,44 @@ +package porcelli.me.git.integration.githook.github; + +import java.io.IOException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import org.kohsuke.github.GHEvent; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GitHub; + +import static java.util.Arrays.asList; + +public class GitHubIntegration { + + private final GitHubCredentials credentials; + + public GitHubIntegration(final GitHubCredentials credentials) { + this.credentials = credentials; + } + + public String createRepository(final String repoName) throws IOException { + final GitHub github = GitHub.connect(); + final GHRepository repo = github.createRepository(repoName) + .description("ssh://localhost:8001/MySpace/" + repoName) + .autoInit(false) + .create(); + + final Map config = new HashMap() {{ + put("url", new URL("http://e5997029.ngrok.io/api/hook/").toExternalForm()); + put("content_type", "json"); + }}; + repo.createHook("web", config, asList(GHEvent.PULL_REQUEST, GHEvent.PUSH), true); + + return repo.getHttpTransportUrl(); + } + + public void createPR(final String repoName, + final String sourceBranch) throws IOException { + final GitHub github = GitHub.connect(); + final GHPullRequest pullRequest = github.getRepository(credentials.getSpace() + "/" + repoName).createPullRequest("PR from RHPAM", sourceBranch, "master", sourceBranch); + } +} diff --git a/bc-github-webhook/pom.xml b/bc-github-webhook/pom.xml new file mode 100644 index 0000000..ba9e2cf --- /dev/null +++ b/bc-github-webhook/pom.xml @@ -0,0 +1,100 @@ + + + + 4.0.0 + + + me.porcelli.bc-github-integration + bc-github-integration-parent + 1.0-SNAPSHOT + ../pom.xml + + + bc-github-webhook + jar + + bc-github-webhook + + + + org.eclipse.jgit + org.eclipse.jgit + + + org.kohsuke + github-api + + + + org.jboss.resteasy + resteasy-jaxrs + + + org.jboss.resteasy + resteasy-servlet-initializer + + + + + javax.servlet + javax.servlet-api + + + org.eclipse.jetty + jetty-servlet + + + + + + + maven-jar-plugin + 3.1.0 + + + + porcelli.me.git.integration.webhook.WebHook + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.0 + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + + porcelli.me.git.integration.webhook.WebHook + + + + + + + + + + diff --git a/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/BCIntegration.java b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/BCIntegration.java new file mode 100644 index 0000000..a312d3a --- /dev/null +++ b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/BCIntegration.java @@ -0,0 +1,92 @@ +package porcelli.me.git.integration.webhook; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; + +import com.jcraft.jsch.Session; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.RemoteAddCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.JschConfigSessionFactory; +import org.eclipse.jgit.transport.OpenSshConfig; +import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.transport.SshSessionFactory; +import org.eclipse.jgit.transport.SshTransport; +import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; +import porcelli.me.git.integration.webhook.model.PullRequestEvent; +import porcelli.me.git.integration.webhook.model.PushEvent; + +public class BCIntegration { + + private final Map repositoryMap = new HashMap<>(); + private final UsernamePasswordCredentialsProvider bcCredentials = new UsernamePasswordCredentialsProvider("porcelli", "pw"); + + public void onPush(final PushEvent pushEvent) throws GitAPIException, URISyntaxException, IOException { + if (!pushEvent.getRef().contains("master")) { + return; + } + final Git git = getGit(pushEvent.getRepository()); + + try { + git.pull().setRemote("origin").setCredentialsProvider(bcCredentials).setRebase(true).call(); + git.pull().setRemote("github").setRebase(true).call(); + git.push().setRemote("origin").setForce(true).setCredentialsProvider(bcCredentials).call(); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + public void onPullRequest(final PullRequestEvent pullRequestEvent) throws GitAPIException, IOException, URISyntaxException { + if (!pullRequestEvent.getAction().equals(PullRequestEvent.Action.CLOSED)){ + return; + } + final String branchName = pullRequestEvent.getPullRequest().getBody(); + + final Git git = getGit(pullRequestEvent.getRepository()); + git.branchDelete().setBranchNames("refs/heads/" + branchName).call(); + + final RefSpec refSpec = new RefSpec().setSource(null).setDestination("refs/heads/" + branchName); + git.push().setRefSpecs(refSpec).setRemote("origin").setCredentialsProvider(bcCredentials).call(); + } + + private Git getGit(porcelli.me.git.integration.webhook.model.Repository repository) throws GitAPIException, URISyntaxException, IOException { + final Git git; + if (!repositoryMap.containsKey(repository.getDescription())) { + final String bcRepo = repository.getDescription(); + + SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() { + @Override + protected void configure(OpenSshConfig.Host host, Session session) { + } + }; + + git = Git.cloneRepository() + .setTransportConfigCallback(transport -> { + SshTransport sshTransport = (SshTransport) transport; + sshTransport.setSshSessionFactory(sshSessionFactory); + }).setURI(bcRepo) + .setCredentialsProvider(bcCredentials) + .setDirectory(tempDir(repository.getFullName())) + .call(); + + final RemoteAddCommand remoteAddCommand = git.remoteAdd(); + remoteAddCommand.setName("github"); + remoteAddCommand.setUri(new URIish(repository.getCloneUrl())); + remoteAddCommand.call(); + repositoryMap.put(repository.getDescription(), git.getRepository()); + } else { + git = new Git(repositoryMap.get(repository.getDescription())); + } + return git; + } + + private File tempDir(String reponame) throws IOException { + return Files.createTempDirectory(new File(System.getProperty("java.io.tmpdir")).toPath(), "temp").resolve(reponame).toFile(); + } +} diff --git a/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/WebHook.java b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/WebHook.java new file mode 100644 index 0000000..0d024fd --- /dev/null +++ b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/WebHook.java @@ -0,0 +1,47 @@ +package porcelli.me.git.integration.webhook; + +import com.jcraft.jsch.JSch; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher; + +public class WebHook { + + private static final String APPLICATION_PATH = "/api"; + private static final String CONTEXT_ROOT = "/"; + + public static void main(String[] args) { + JSch.setConfig("StrictHostKeyChecking", "no"); + try { + new WebHook().run(); + } catch (Throwable t) { + t.printStackTrace(); + } + } + + public void run() throws Exception { + + final int port = 9090; + final Server server = new Server(port); + + // Setup the basic Application "context" at "/". + // This is also known as the handler tree (in Jetty speak). + final ServletContextHandler context = new ServletContextHandler(server, CONTEXT_ROOT); + + // Setup RESTEasy's HttpServletDispatcher at "/api/*". + final ServletHolder restEasyServlet = new ServletHolder(new HttpServletDispatcher()); + restEasyServlet.setInitParameter("resteasy.servlet.mapping.prefix", APPLICATION_PATH); + restEasyServlet.setInitParameter("javax.ws.rs.Application", + "porcelli.me.git.integration.webhook.WebHookApplication"); + context.addServlet(restEasyServlet, APPLICATION_PATH + "/*"); + + // Setup the DefaultServlet at "/". + final ServletHolder defaultServlet = new ServletHolder(new DefaultServlet()); + context.addServlet(defaultServlet, CONTEXT_ROOT); + + server.start(); + server.join(); + } +} diff --git a/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/WebHookApplication.java b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/WebHookApplication.java new file mode 100644 index 0000000..63ac8da --- /dev/null +++ b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/WebHookApplication.java @@ -0,0 +1,21 @@ +package porcelli.me.git.integration.webhook; + +import java.util.HashSet; +import java.util.Set; + +import javax.ws.rs.core.Application; + +import porcelli.me.git.integration.webhook.resource.HookResource; + +public class WebHookApplication extends Application { + + public WebHookApplication() { + } + + @Override + public Set getSingletons() { + return new HashSet(){{ + add(new HookResource()); + }}; + } +} \ No newline at end of file diff --git a/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/json/MappingModule.java b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/json/MappingModule.java new file mode 100644 index 0000000..ba28fff --- /dev/null +++ b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/json/MappingModule.java @@ -0,0 +1,99 @@ +package porcelli.me.git.integration.webhook.json; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleDeserializers; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import porcelli.me.git.integration.webhook.model.Payload; +import porcelli.me.git.integration.webhook.model.PullRequestEvent; + +public class MappingModule extends SimpleModule { + + private static class IOBuilder { + + public static IOBuilder builder(Class typeClass, Class modelClass) { + return new IOBuilder<>(typeClass, modelClass); + } + + private final Class typeClass; + private final Class modelClass; + private final Map> targetModelClasses; + + private IOBuilder(Class typeClass, Class modelClass) { + this.typeClass = typeClass; + this.modelClass = modelClass; + this.targetModelClasses = new HashMap<>(); + } + + public IOBuilder defineType(T type, Class modelType) { + this.targetModelClasses.put(type, modelType); + return this; + } + + public SubtypeDeserializer buildDeserializer() { + return new SubtypeDeserializer<>(typeClass, modelClass, targetModelClasses); + } + } + + private static class SubtypeDeserializer extends StdDeserializer { + + private final Class typeClass; + private final Map> targetModelClasses; + + SubtypeDeserializer(Class typeClass, Class modelClass, Map> targetModelClasses) { + super(modelClass); + this.typeClass = typeClass; + this.targetModelClasses = targetModelClasses; + } + + @Override + public M deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + ObjectMapper mapper = (ObjectMapper) jp.getCodec(); + ObjectNode root = mapper.readTree(jp); + + JsonNode typeNode = root.get("type"); + if (typeNode == null) { + throw new IllegalArgumentException("Must specify a " + typeClass + " in the \"type\" field!"); + } + + String typeValue = typeNode.textValue(); + for (Entry> entry : targetModelClasses.entrySet()) { + T key = entry.getKey(); + Class value = entry.getValue(); + + if (key.toString().equalsIgnoreCase(typeValue)) { + JsonParser traverse = root.traverse(); + traverse.setCodec(mapper); + return mapper.readValue(traverse, value); + } + } + throw new IllegalArgumentException(typeClass + ": " + typeValue + " is unsupported!"); + } + } + + @Override + public void setupModule(SetupContext context) { + SimpleDeserializers deserializers = new SimpleDeserializers(); + context.addDeserializers(deserializers); + addCustomPayloadDeserialization(deserializers); + } + + private void addCustomPayloadDeserialization(SimpleDeserializers deserializers) { + Class typeClass = Payload.EventType.class; + Class modelClass = Payload.class; + + IOBuilder io = IOBuilder.builder(typeClass, modelClass) + .defineType(Payload.EventType.PULL_REQUEST, PullRequestEvent.class); + deserializers.addDeserializer(modelClass, io.buildDeserializer()); + } +} \ No newline at end of file diff --git a/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/Head.java b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/Head.java new file mode 100644 index 0000000..d8990a4 --- /dev/null +++ b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/Head.java @@ -0,0 +1,53 @@ +package porcelli.me.git.integration.webhook.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class Head { + + private String label; + private String ref; + private String sha; + private User user; + private Repository repo; + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getRef() { + return ref; + } + + public void setRef(String ref) { + this.ref = ref; + } + + public String getSha() { + return sha; + } + + public void setSha(String sha) { + this.sha = sha; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Repository getRepo() { + return repo; + } + + public void setRepo(Repository repo) { + this.repo = repo; + } +} diff --git a/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/Payload.java b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/Payload.java new file mode 100644 index 0000000..3675378 --- /dev/null +++ b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/Payload.java @@ -0,0 +1,146 @@ +package porcelli.me.git.integration.webhook.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public abstract class Payload { + + /** + * When configuring a webhook, you can choose which events you would like to + * receive payloads for. You can even opt-in to all current and future + * events. Only subscribing to the specific events you plan on handling is + * useful for limiting the number of HTTP requests to your server. You can + * change the list of subscribed events through the API or UI anytime. By + * default, webhooks are only subscribed to the push event. + * + *

+ * Each event corresponds to a certain set of actions that can happen to + * your organization and/or repository. For example, if you subscribe to the + * issues event you’ll receive detailed payloads every time an issue is + * opened, closed, labeled, etc. + *

+ */ + public static enum EventType { + /** + * Any time a Commit is commented on. + */ + COMMIT_COMMENT, + + /** + * Any time a Branch or Tag is created. + */ + CREATE, + + /** + * Any time a Branch or Tag is deleted. + */ + DELETE, + + /** + * Any time a Repository has a new deployment created from the API. + */ + DEPLOYMENT, + + /** + * Any time a deployment for a Repository has a status update from the API. + */ + DEPLOYMENT_STATUS, + + /** + * Any time a Repository is forked. + */ + FORK, + + /** + * Any time a Wiki page is updated. + */ + GOLLUM, + + /** + * Any time an Issue is commented on. + */ + ISSUE_COMMENT, + + /** + * Any time an Issue is assigned, unassigned, labeled, unlabeled, opened, closed, or reopened. + */ + ISSUES, + + /** + * Any time a User is added as a collaborator to a non-Organization Repository. + */ + MEMBER, + + /** + * Any time a User is added or removed from a team. Organization hooks only. + */ + MEMBERSHIP, + + /** + * Any time a Pages site is built or results in a failed build. + */ + PAGE_BUILD, + + PING, + + /** + * Any time a Repository changes from private to public. + */ + PUBLIC, + + /** + * Any time a Commit is commented on while inside a Pull Request review (the Files Changed tab). + */ + PULL_REQUEST_REVIEW_COMMENT, + + /** + * Any time a Pull Request is assigned, unassigned, labeled, unlabeled, + * opened, closed, reopened, or synchronized (updated due to a new push + * in the branch that the pull request is tracking). + */ + PULL_REQUEST, + + /** + * Any Git push to a Repository, including editing tags or branches. + * Commits via API actions that update references are also counted. This + * is the default event. + */ + PUSH, + + /** + * Any time a Repository is created. Organization hooks only. + */ + REPOSITORY, + + /** + * Any time a Release is published in a Repository. + */ + RELEASE, + + /** + * Any time a Repository has a status update from the API + */ + STATUS, + + /** + * Any time a team is added or modified on a Repository. + */ + TEAM_ADD, + + /** + * Any time a User watches a Repository. + */ + WATCH; + + } + + @JsonIgnore + private EventType eventType; + + public EventType getEventType() { + return eventType; + } + + public void setEventType(EventType eventType) { + this.eventType = eventType; + } +} diff --git a/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/PullRequest.java b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/PullRequest.java new file mode 100644 index 0000000..75947fd --- /dev/null +++ b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/PullRequest.java @@ -0,0 +1,180 @@ +package porcelli.me.git.integration.webhook.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PullRequest { + + private String url; + private Integer id; + @JsonProperty("html_url") + private String htmlUrl; + @JsonProperty("diff_url") + private String diffUrl; + @JsonProperty("patch_url") + private String patchUrl; + @JsonProperty("issue_url") + private String issueUrl; + private Integer number; + private State state; + private boolean locked; + private String title; + private String body; + private User user; + private Head head; + private Head base; + private boolean merged; + + static enum State { + OPEN, + CLOSED; + + @JsonCreator + public static State forValue(String value) { + return State.valueOf(value.toUpperCase()); + } + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getHtmlUrl() { + return htmlUrl; + } + + public void setHtmlUrl(String htmlUrl) { + this.htmlUrl = htmlUrl; + } + + public String getDiffUrl() { + return diffUrl; + } + + public void setDiffUrl(String diffUrl) { + this.diffUrl = diffUrl; + } + + public String getPatchUrl() { + return patchUrl; + } + + public void setPatchUrl(String patchUrl) { + this.patchUrl = patchUrl; + } + + public String getIssueUrl() { + return issueUrl; + } + + public void setIssueUrl(String issueUrl) { + this.issueUrl = issueUrl; + } + + public Integer getNumber() { + return number; + } + + public void setNumber(Integer number) { + this.number = number; + } + + public State getState() { + return state; + } + + public void setState(State state) { + this.state = state; + } + + public boolean isLocked() { + return locked; + } + + public void setLocked(boolean locked) { + this.locked = locked; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Head getHead() { + return head; + } + + public void setHead(Head head) { + this.head = head; + } + + public Head getBase() { + return base; + } + + public void setBase(Head base) { + this.base = base; + } + + public boolean isMerged() { + return merged; + } + + public void setMerged(boolean merged) { + this.merged = merged; + } + + @Override + public String toString() { + return "PullRequest{" + + "url='" + url + '\'' + + ", id=" + id + + ", htmlUrl='" + htmlUrl + '\'' + + ", diffUrl='" + diffUrl + '\'' + + ", patchUrl='" + patchUrl + '\'' + + ", issueUrl='" + issueUrl + '\'' + + ", number=" + number + + ", state=" + state + + ", locked=" + locked + + ", title='" + title + '\'' + + ", body='" + body + '\'' + + ", user=" + user + + ", head=" + head + + ", base=" + base + + ", merged=" + merged + + '}'; + } +} \ No newline at end of file diff --git a/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/PullRequestEvent.java b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/PullRequestEvent.java new file mode 100644 index 0000000..cc30aba --- /dev/null +++ b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/PullRequestEvent.java @@ -0,0 +1,89 @@ +package porcelli.me.git.integration.webhook.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class PullRequestEvent extends Payload { + + public PullRequestEvent() { + setEventType(EventType.PULL_REQUEST); + } + + private Action action; + + private Integer number; + + @JsonProperty("pull_request") + private PullRequest pullRequest; + + private Repository repository; + + private User sender; + + public static enum Action { + ASSGIGNED, + UNASSIGNED, + LABELED, + UNLABELED, + OPENED, + CLOSED, + REOPENED, + SYNCHRONIZE; + + @JsonCreator + public static Action forValue(String value) { + return Action.valueOf(value.toUpperCase()); + } + } + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + public Integer getNumber() { + return number; + } + + public void setNumber(Integer number) { + this.number = number; + } + + public PullRequest getPullRequest() { + return pullRequest; + } + + public void setPullRequest(PullRequest pullRequest) { + this.pullRequest = pullRequest; + } + + public Repository getRepository() { + return repository; + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + public User getSender() { + return sender; + } + + public void setSender(User sender) { + this.sender = sender; + } + + @Override + public String toString() { + return "PullRequestEvent{" + + "action=" + action + + ", number=" + number + + ", pullRequest=" + pullRequest + + ", repository=" + repository + + ", sender=" + sender + + '}'; + } +} diff --git a/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/PushEvent.java b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/PushEvent.java new file mode 100644 index 0000000..2d9c7ac --- /dev/null +++ b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/PushEvent.java @@ -0,0 +1,50 @@ +package porcelli.me.git.integration.webhook.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PushEvent extends Payload { + + public PushEvent() { + setEventType(EventType.PUSH); + } + + private String ref; + + private Repository repository; + + private User sender; + + public String getRef() { + return ref; + } + + public void setRef(String ref) { + this.ref = ref; + } + + public Repository getRepository() { + return repository; + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + public User getSender() { + return sender; + } + + public void setSender(User sender) { + this.sender = sender; + } + + @Override + public String toString() { + return "PushEvent{" + + "ref='" + ref + '\'' + + ", repository=" + repository + + ", sender=" + sender + + '}'; + } +} diff --git a/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/Repository.java b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/Repository.java new file mode 100644 index 0000000..af29319 --- /dev/null +++ b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/Repository.java @@ -0,0 +1,109 @@ +package porcelli.me.git.integration.webhook.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class Repository { + + private Integer id; + private String name; + @JsonProperty("full_name") + private String fullName; + private User owner; + @JsonProperty("private") + private boolean privateRepository; + @JsonProperty("html_url") + private String htmlUrl; + @JsonProperty("clone_url") + private String cloneUrl; + private String description; + private boolean fork; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public User getOwner() { + return owner; + } + + public void setOwner(User owner) { + this.owner = owner; + } + + public boolean isPrivateRepository() { + return privateRepository; + } + + public void setPrivateRepository(boolean privateRepository) { + this.privateRepository = privateRepository; + } + + public String getHtmlUrl() { + return htmlUrl; + } + + public void setHtmlUrl(String htmlUrl) { + this.htmlUrl = htmlUrl; + } + + public String getCloneUrl() { + return cloneUrl; + } + + public void setCloneUrl(String cloneUrl) { + this.cloneUrl = cloneUrl; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isFork() { + return fork; + } + + public void setFork(boolean fork) { + this.fork = fork; + } + + @Override + public String toString() { + return "Repository{" + + "id=" + id + + ", name='" + name + '\'' + + ", fullName='" + fullName + '\'' + + ", owner=" + owner + + ", privateRepository=" + privateRepository + + ", htmlUrl='" + htmlUrl + '\'' + + ", cloneUrl='" + cloneUrl + '\'' + + ", description='" + description + '\'' + + ", fork=" + fork + + '}'; + } +} diff --git a/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/User.java b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/User.java new file mode 100644 index 0000000..574ac2f --- /dev/null +++ b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/model/User.java @@ -0,0 +1,34 @@ +package porcelli.me.git.integration.webhook.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class User { + + private String login; + private Integer id; + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + @Override + public String toString() { + return "User{" + + "login='" + login + '\'' + + ", id=" + id + + '}'; + } +} diff --git a/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/resource/HookResource.java b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/resource/HookResource.java new file mode 100644 index 0000000..3ad4249 --- /dev/null +++ b/bc-github-webhook/src/main/java/porcelli/me/git/integration/webhook/resource/HookResource.java @@ -0,0 +1,73 @@ +package porcelli.me.git.integration.webhook.resource; + +import java.io.InputStream; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import porcelli.me.git.integration.webhook.BCIntegration; +import porcelli.me.git.integration.webhook.json.MappingModule; +import porcelli.me.git.integration.webhook.model.Payload; +import porcelli.me.git.integration.webhook.model.PullRequestEvent; +import porcelli.me.git.integration.webhook.model.PushEvent; + +@Path("/hook") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class HookResource { + + private final ObjectMapper objectMapper; + private final BCIntegration bcIntegration; + + public HookResource() { + final MappingModule module = new MappingModule(); + objectMapper = new ObjectMapper(); + objectMapper.registerModule(module); + bcIntegration = new BCIntegration(); + } + + @POST + @Path("/") + public Response post(@HeaderParam("X-Github-Event") String event, + @Context HttpServletRequest request) { + try (InputStream in = request.getInputStream()) { + Payload.EventType type = Payload.EventType.valueOf(event.toUpperCase()); + + switch (type) { + case PULL_REQUEST: + bcIntegration.onPullRequest(objectMapper.readValue(in, PullRequestEvent.class)); + break; + case PUSH: + bcIntegration.onPush(objectMapper.readValue(in, PushEvent.class)); + break; + default: + break; + } + + return Response.ok().build(); + } catch (JsonParseException | JsonMappingException e) { + throw new InternalServerErrorException(e.getMessage(), Response + .status(Status.INTERNAL_SERVER_ERROR) + .entity(e.getMessage() + "\n").build(), e); + } catch (IllegalArgumentException e) { + throw new BadRequestException(e); + } catch (Exception e) { + throw new InternalServerErrorException(Response + .status(Status.INTERNAL_SERVER_ERROR) + .entity(e.getMessage()).build(), e); + } + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..39ff37c --- /dev/null +++ b/pom.xml @@ -0,0 +1,106 @@ + + + + 4.0.0 + + me.porcelli.bc-github-integration + bc-github-integration-parent + 1.0-SNAPSHOT + + Sample Business Central Git Integration with GitHub - Using GitHooks and WebHooks + pom + + + UTF-8 + 1.8 + 1.8 + + + + + + org.eclipse.jgit + org.eclipse.jgit + 4.8.0.201706111038-r + + + org.kohsuke + github-api + 1.94 + + + org.jboss.resteasy + resteasy-jaxrs + 3.0.19.Final + + + org.jboss.resteasy + resteasy-servlet-initializer + 3.0.19.Final + + + javax.servlet + javax.servlet-api + 3.1.0 + + + org.eclipse.jetty + jetty-servlet + 9.3.14.v20161028 + + + + + + bc-github-githook + bc-github-webhook + + + + + + maven-jar-plugin + 3.1.0 + + + + porcelli.me.git.integration.githook.GitHook + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.0 + + + package + + shade + + + + + junit:junit:jar: + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + +