com.querydsl.webhooks.GithubReviewWindow.java Source code

Java tutorial

Introduction

Here is the source code for com.querydsl.webhooks.GithubReviewWindow.java

Source

/*
 * Copyright 2016 The Querydsl Team.
 *
 * 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 com.querydsl.webhooks;

import static java.time.ZonedDateTime.now;

import java.io.IOException;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ScheduledFuture;

import org.kohsuke.github.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler;

import com.github.shredder121.gh_event_api.GHEventApiServer;
import com.github.shredder121.gh_event_api.handler.pull_request.*;
import com.github.shredder121.gh_event_api.model.PullRequest;
import com.github.shredder121.gh_event_api.model.Ref;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.collect.Maps;

/**
 * GitHub Review Window - A GitHub Webhook Implementation.
 *
 * <p>
 * When using Pull Requests, it's often necessary to review their contents.
 *
 * <p>
 * This piece of software adds a commit status to the PR's head commit, essentially blocking
 * The mergability until the duration of the review window has passed.
 *
 * <p>
 * usage:
 *   {@code java -Dduration=(defaultDurationString) [-Dduration.(labelName)=(durationString)] -jar gh-review-window-(version)-full.jar }
 *
 * @author Shredder121
 */
@SpringBootApplication
public class GithubReviewWindow {

    private static final Logger logger = LoggerFactory.getLogger(GithubReviewWindow.class);

    private static final GitHub github;

    private final TaskScheduler taskScheduler = new ConcurrentTaskScheduler();

    static {
        try {
            github = GitHub.connect();
        } catch (IOException ex) {
            throw Throwables.propagate(ex);
        }
    }

    public static void main(String... args) {
        GHEventApiServer.start(GithubReviewWindow.class, args);
    }

    @Bean
    public PullRequestHandler reviewWindowHandler(Environment environment) {
        Duration defaultReviewWindow = Duration.parse(environment.getRequiredProperty("duration")); //duration is the default window
        Map<String, ScheduledFuture<?>> asyncTasks = Maps.newConcurrentMap();

        return payload -> {
            PullRequest pullRequest = payload.getPullRequest();
            Ref head = pullRequest.getHead();

            try {
                GHRepository repository = github.getRepository(payload.getRepository().getFullName());
                Collection<GHLabel> labels = repository.getIssue(pullRequest.getNumber()).getLabels();

                Duration reviewTime = labels.stream().map(label -> "duration." + label.getName()) //for all duration.[label] properties
                        .map(environment::getProperty).filter(Objects::nonNull) //look for a Duration
                        .findFirst().map(Duration::parse).orElse(defaultReviewWindow); //if none found, use the default window

                ZonedDateTime creationTime = pullRequest.getCreatedAt();
                ZonedDateTime windowCloseTime = creationTime.plus(reviewTime);

                boolean windowPassed = now().isAfter(windowCloseTime);
                logger.info("creationTime({}) + reviewTime({}) = windowCloseTime({}), so windowPassed = {}",
                        creationTime, reviewTime, windowCloseTime, windowPassed);

                if (windowPassed) {
                    completeAndCleanUp(asyncTasks, repository, head);
                } else {
                    createPendingMessage(repository, head, reviewTime);

                    ScheduledFuture<?> scheduledTask = taskScheduler.schedule(
                            () -> completeAndCleanUp(asyncTasks, repository, head),
                            Date.from(windowCloseTime.toInstant()));

                    replaceCompletionTask(asyncTasks, scheduledTask, head);
                }
            } catch (IOException ex) {
                throw Throwables.propagate(ex);
            }
        };
    }

    @VisibleForTesting
    protected static String makeHumanReadable(Duration duration) {
        StringBuilder output = new StringBuilder();
        duration = truncateAndAppend(duration, duration.toDays(), ChronoUnit.DAYS, "day", output);
        duration = truncateAndAppend(duration, duration.toHours(), ChronoUnit.HOURS, "hour", output);
        duration = truncateAndAppend(duration, duration.toMinutes(), ChronoUnit.MINUTES, "minute", output);
        duration = truncateAndAppend(duration, duration.getSeconds(), ChronoUnit.SECONDS, "second", output);
        return output.toString().trim();
    }

    private static Duration truncateAndAppend(Duration duration, long amount, ChronoUnit unit, String description,
            StringBuilder builder) {

        if (amount > 0) {
            builder.append(amount).append(' ').append(description).append(' ');
        }
        return duration.minus(amount, unit);
    }

    private static void completeAndCleanUp(Map<String, ?> tasks, GHRepository repo, Ref head) {
        createSuccessMessage(repo, head);
        tasks.remove(head.getSha());
    }

    private static void replaceCompletionTask(Map<String, ScheduledFuture<?>> tasks,
            ScheduledFuture<?> completionTask, Ref head) {

        boolean interrupt = false;
        tasks.merge(head.getSha(), completionTask, (oldTask, newTask) -> {
            oldTask.cancel(interrupt);
            return newTask;
        });
    }

    private static void createSuccessMessage(GHRepository repo, Ref commit) {
        createStatusMessage(repo, commit, GHCommitState.SUCCESS, "The review window has passed");
    }

    private static void createPendingMessage(GHRepository repo, Ref commit, Duration reviewWindow) {
        createStatusMessage(repo, commit, GHCommitState.PENDING,
                "The " + makeHumanReadable(reviewWindow) + " review window has not passed");
    }

    private static void createStatusMessage(GHRepository repo, Ref commit, GHCommitState state, String message) {
        try {
            repo.createCommitStatus(commit.getSha(), state, null, message, "review-window");
        } catch (IOException ex) {
            logger.warn("Exception updating status", ex);
        }
    }

}