package com.hubspot.blazar.util;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.kohsuke.github.GHRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;
import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;
import com.hubspot.blazar.base.BranchSetting;
import com.hubspot.blazar.base.BuildOptions;
import com.hubspot.blazar.base.BuildTrigger;
import com.hubspot.blazar.base.GitInfo;
import com.hubspot.blazar.data.service.BranchService;
import com.hubspot.blazar.data.service.BranchSettingsService;
import com.hubspot.blazar.data.service.RepositoryBuildService;
import com.hubspot.blazar.github.GitHubProtos.CreateEvent;
import com.hubspot.blazar.github.GitHubProtos.DeleteEvent;
import com.hubspot.blazar.github.GitHubProtos.PushEvent;
import com.hubspot.blazar.github.GitHubProtos.Repository;
@Singleton
public class GitHubWebhookHandler {
private static final Logger LOG = LoggerFactory.getLogger(GitHubWebhookHandler.class);
private final BranchService branchService;
private final RepositoryBuildService repositoryBuildService;
private final BranchSettingsService branchSettingsService;
private final GitHubHelper gitHubHelper;
private final Set<String> whitelist;
private final Set<String> blacklist;
@Inject
public GitHubWebhookHandler(BranchService branchService,
RepositoryBuildService repositoryBuildService,
BranchSettingsService branchSettingsService,
GitHubHelper gitHubHelper,
@Named("whitelist") Set<String> whitelist,
@Named("blacklist") Set<String> blacklist,
EventBus eventBus) {
this.branchService = branchService;
this.repositoryBuildService = repositoryBuildService;
this.branchSettingsService = branchSettingsService;
this.gitHubHelper = gitHubHelper;
this.whitelist = whitelist;
this.blacklist = blacklist;
LOG.debug("Registering to event bus {}", eventBus);
eventBus.register(this);
}
//TODO: Probably this is redundant since we also register the branch through the push event
// leaving it here for now and may remove at some point
@Subscribe
public void handleCreateEvent(CreateEvent createEvent) throws IOException {
if (!createEvent.hasRepository()) {
return;
}
if (blacklist.contains(createEvent.getRepository().getName())) {
LOG.info("Ignoring hook from repo {} because it is in the blacklist.", createEvent.getRepository().getName());
return;
}
if ("branch".equalsIgnoreCase(createEvent.getRefType())) {
GitInfo gitInfo = gitInfo(createEvent);
if (isOptedIn(gitInfo)) {
branchService.upsert(gitInfo);
}
}
}
@Subscribe
public void handleDeleteEvent(DeleteEvent deleteEvent) {
if (!deleteEvent.hasRepository()) {
return;
}
if (blacklist.contains(deleteEvent.getRepository().getName())) {
LOG.info("Ignoring hook from repo {} because it is in the blacklist.", deleteEvent.getRepository().getName());
return;
}
if ("branch".equalsIgnoreCase(deleteEvent.getRefType())) {
branchService.deactivate(gitInfo(deleteEvent));
}
}
@Subscribe
public void handlePushEvent(PushEvent pushEvent) throws IOException {
if (!pushEvent.hasRepository()) {
return;
}
if (blacklist.contains(pushEvent.getRepository().getName())) {
LOG.info("Ignoring hook from repo {} because it is in the blacklist.", pushEvent.getRepository().getName());
return;
}
if (!pushEvent.getRef().startsWith("refs/tags/") && !pushEvent.getDeleted()) {
GitInfo gitInfo = gitInfo(pushEvent);
if (!isOptedIn(gitInfo)) {
LOG.debug("Not {}#{} is not opted in to Blazar", gitInfo.getFullRepositoryName(), gitInfo.getBranch());
return;
}
gitInfo = branchService.upsert(gitInfo);
Optional<BranchSetting> branchSetting = branchSettingsService.getByBranchId(gitInfo.getId().get());
if (branchSetting.isPresent() && branchSetting.get().isTriggerInterProjectBuilds()) {
BuildOptions options = new BuildOptions(ImmutableSet.<Integer>of(), BuildOptions.BuildDownstreams.INTER_PROJECT, false);
repositoryBuildService.enqueue(gitInfo, BuildTrigger.forCommit(pushEvent.getAfter()), options);
} else {
repositoryBuildService.enqueue(gitInfo, BuildTrigger.forCommit(pushEvent.getAfter()), BuildOptions.defaultOptions());
}
}
}
private boolean isOptedIn(GitInfo gitInfo) throws IOException {
return whitelist.contains(gitInfo.getRepository()) || ( whitelist.isEmpty() && blazarConfigExists(gitInfo));
}
private boolean blazarConfigExists(GitInfo gitInfo) throws IOException {
GHRepository repository = gitHubHelper.repositoryFor(gitInfo);
try {
gitHubHelper.contentsFor(".blazar-enabled", repository, gitInfo);
return true;
} catch (FileNotFoundException e) {
try {
String config = gitHubHelper.contentsFor(".blazar.yaml", repository, gitInfo);
return config.contains("enabled: true") || !config.contains("disabled: true");
} catch (FileNotFoundException e1) {
return false;
}
}
}
private GitInfo gitInfo(CreateEvent createEvent) {
return gitInfo(createEvent.getRepository(), createEvent.getRef(), true);
}
private GitInfo gitInfo(DeleteEvent deleteEvent) {
return gitInfo(deleteEvent.getRepository(), deleteEvent.getRef(), false);
}
private GitInfo gitInfo(PushEvent pushEvent) {
// We always send active = true to reactivate branches that have been deleted and deactivated
return gitInfo(pushEvent.getRepository(), pushEvent.getRef(), true);
}
private GitInfo gitInfo(Repository repository, String ref, boolean active) {
String host = URI.create(repository.getUrl()).getHost();
if ("api.github.com".equals(host)) {
host = "github.com";
}
String fullName = repository.getFullName();
String organization = fullName.substring(0, fullName.indexOf('/'));
String repositoryName = fullName.substring(fullName.indexOf('/') + 1);
int repositoryId = repository.getId();
String branch = branchFromRef(ref);
return new GitInfo(Optional.<Integer>absent(), host, organization, repositoryName, repositoryId, branch, active, System.currentTimeMillis(), System.currentTimeMillis());
}
private static String branchFromRef(String ref) {
return ref.startsWith("refs/heads/") ? ref.substring("refs/heads/".length()) : ref;
}
}