package org.kohsuke.github; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.databind.exc.MismatchedInputException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.io.IOException; import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; /** * Statistics for a GitHub repository. * * @author Martin van Zijl */ public class GHRepositoryStatistics extends GitHubInteractiveObject { private final GHRepository repo; private static final int MAX_WAIT_ITERATIONS = 3; private static final int WAIT_SLEEP_INTERVAL = 5000; /** * Instantiates a new Gh repository statistics. * * @param repo * the repo */ public GHRepositoryStatistics(GHRepository repo) { this.repo = repo; this.root = repo.root; } /** * Get contributors list with additions, deletions, and commit count. See * https://developer.github.com/v3/repos/statistics/#get-contributors-list-with-additions-deletions-and-commit-counts * * @return the contributor stats * @throws IOException * the io exception * @throws InterruptedException * the interrupted exception */ public PagedIterable getContributorStats() throws IOException, InterruptedException { return getContributorStats(true); } /** * Gets contributor stats. * * @param waitTillReady * Whether to sleep the thread if necessary until the statistics are ready. This is true by default. * @return the contributor stats * @throws IOException * the io exception * @throws InterruptedException * the interrupted exception */ @BetaApi @Deprecated @SuppressWarnings("SleepWhileInLoop") @SuppressFBWarnings(value = { "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE" }, justification = "JSON API") public PagedIterable getContributorStats(boolean waitTillReady) throws IOException, InterruptedException { PagedIterable stats = getContributorStatsImpl(); if (stats == null && waitTillReady) { for (int i = 0; i < MAX_WAIT_ITERATIONS; i += 1) { // Wait a few seconds and try again. Thread.sleep(WAIT_SLEEP_INTERVAL); stats = getContributorStatsImpl(); if (stats != null) { break; } } } return stats; } /** * This gets the actual statistics from the server. Returns null if they are still being cached. */ private PagedIterable getContributorStatsImpl() throws IOException { return root.createRequest() .withUrlPath(getApiTailUrl("contributors")) .toIterable(ContributorStats[].class, item -> item.wrapUp(root)); } /** * The type ContributorStats. */ @SuppressFBWarnings( value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD", "URF_UNREAD_FIELD" }, justification = "JSON API") public static class ContributorStats extends GHObject { private GHUser author; private int total; private List weeks; @Override public URL getHtmlUrl() throws IOException { throw new UnsupportedOperationException("Not supported yet."); } /** * Gets root. * * @return the root */ public GitHub getRoot() { return root; } /** * Gets author. * * @return The author described by these statistics. */ public GHUser getAuthor() { return author; } /** * Gets total. * * @return The total number of commits authored by the contributor. */ public int getTotal() { return total; } /** * Convenience method to look up week with particular timestamp. * * @param timestamp * The timestamp to look for. * @return The week starting with the given timestamp. Throws an exception if it is not found. * @throws NoSuchElementException * the no such element exception */ public Week getWeek(long timestamp) throws NoSuchElementException { // maybe store the weeks in a map to make this more efficient? for (Week week : weeks) { if (week.getWeekTimestamp() == timestamp) { return week; } } // this is safer than returning null throw new NoSuchElementException(); } /** * Gets weeks. * * @return The total number of commits authored by the contributor. */ public List getWeeks() { return weeks; } @Override public String toString() { return author.getLogin() + " made " + String.valueOf(total) + " contributions over " + String.valueOf(weeks.size()) + " weeks"; } /** * The type Week. */ @SuppressFBWarnings( value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD", "URF_UNREAD_FIELD" }, justification = "JSON API") public static class Week { private long w; private int a; private int d; private int c; /** * Gets week timestamp. * * @return Start of the week, as a UNIX timestamp. */ public long getWeekTimestamp() { return w; } /** * Gets number of additions. * * @return The number of additions for the week. */ public int getNumberOfAdditions() { return a; } /** * Gets number of deletions. * * @return The number of deletions for the week. */ public int getNumberOfDeletions() { return d; } /** * Gets number of commits. * * @return The number of commits for the week. */ public int getNumberOfCommits() { return c; } @Override public String toString() { return String.format("Week starting %d - Additions: %d, Deletions: %d, Commits: %d", w, a, d, c); } } ContributorStats wrapUp(GitHub root) { this.root = root; return this; } } /** * Get the last year of commit activity data. See * https://developer.github.com/v3/repos/statistics/#get-the-last-year-of-commit-activity-data * * @return the commit activity * @throws IOException * the io exception */ public PagedIterable getCommitActivity() throws IOException { return root.createRequest() .withUrlPath(getApiTailUrl("commit_activity")) .toIterable(CommitActivity[].class, item -> item.wrapUp(root)); } /** * The type CommitActivity. */ @SuppressFBWarnings( value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD" }, justification = "JSON API") public static class CommitActivity extends GHObject { private List days; private int total; private long week; /** * Gets days. * * @return The number of commits for each day of the week. 0 = Sunday, 1 = Monday, etc. */ public List getDays() { return days; } /** * Gets total. * * @return The total number of commits for the week. */ public int getTotal() { return total; } /** * Gets week. * * @return The start of the week as a UNIX timestamp. */ public long getWeek() { return week; } CommitActivity wrapUp(GitHub root) { this.root = root; return this; } /** * Gets root. * * @return the root */ public GitHub getRoot() { return root; } @Override public URL getHtmlUrl() throws IOException { throw new UnsupportedOperationException("Not supported yet."); } } /** * Get the number of additions and deletions per week. See * https://developer.github.com/v3/repos/statistics/#get-the-number-of-additions-and-deletions-per-week * * @return the code frequency * @throws IOException * the io exception */ public List getCodeFrequency() throws IOException { try { CodeFrequency[] list = root.createRequest() .withUrlPath(getApiTailUrl("code_frequency")) .fetch(CodeFrequency[].class); return Arrays.asList(list); } catch (MismatchedInputException e) { // This sometimes happens when retrieving code frequency statistics // for a repository for the first time. It is probably still being // generated, so return null. return null; } } /** * The type CodeFrequency. */ public static class CodeFrequency { private final int week; private final int additions; private final int deletions; @JsonCreator(mode = JsonCreator.Mode.DELEGATING) private CodeFrequency(List item) { week = item.get(0); additions = item.get(1); deletions = item.get(2); } /** * Gets week timestamp. * * @return The start of the week as a UNIX timestamp. */ public int getWeekTimestamp() { return week; } /** * Gets additions. * * @return The number of additions for the week. */ public long getAdditions() { return additions; } /** * Gets deletions. * * @return The number of deletions for the week. NOTE: This will be a NEGATIVE number. */ public long getDeletions() { // TODO: Perhaps return Math.abs(deletions), // since most developers may not expect a negative number. return deletions; } @Override public String toString() { return "Week starting " + getWeekTimestamp() + " has " + getAdditions() + " additions and " + Math.abs(getDeletions()) + " deletions"; } } /** * Get the weekly commit count for the repository owner and everyone else. See * https://developer.github.com/v3/repos/statistics/#get-the-weekly-commit-count-for-the-repository-owner-and-everyone-else * * @return the participation * @throws IOException * the io exception */ public Participation getParticipation() throws IOException { return root.createRequest().withUrlPath(getApiTailUrl("participation")).fetch(Participation.class); } /** * The type Participation. */ public static class Participation extends GHObject { private List all; private List owner; @Override public URL getHtmlUrl() throws IOException { throw new UnsupportedOperationException("Not supported yet."); } /** * Gets root. * * @return the root */ public GitHub getRoot() { return root; } /** * Gets all commits. * * @return The list of commit counts for everyone combined, for the last 52 weeks. */ public List getAllCommits() { return Collections.unmodifiableList(all); } /** * Gets owner commits. * * @return The list of commit counts for the owner, for the last 52 weeks. */ public List getOwnerCommits() { return Collections.unmodifiableList(owner); } Participation wrapUp(GitHub root) { this.root = root; return this; } } /** * Get the number of commits per hour in each day. See * https://developer.github.com/v3/repos/statistics/#get-the-number-of-commits-per-hour-in-each-day * * @return the punch card * @throws IOException * the io exception */ public List getPunchCard() throws IOException { PunchCardItem[] list = root.createRequest() .withUrlPath(getApiTailUrl("punch_card")) .fetch(PunchCardItem[].class); return Arrays.asList(list); } /** * The type PunchCardItem. */ public static class PunchCardItem { private final int dayOfWeek; private final int hourOfDay; private final int numberOfCommits; @JsonCreator(mode = JsonCreator.Mode.DELEGATING) private PunchCardItem(List item) { dayOfWeek = item.get(0); hourOfDay = item.get(1); numberOfCommits = item.get(2); } /** * Gets day of week. * * @return The day of the week. 0 = Sunday, 1 = Monday, etc. */ public int getDayOfWeek() { return dayOfWeek; } /** * Gets hour of day. * * @return The hour of the day from 0 to 23. */ public long getHourOfDay() { return hourOfDay; } /** * Gets number of commits. * * @return The number of commits for the day and hour. */ public long getNumberOfCommits() { return numberOfCommits; } public String toString() { return "Day " + getDayOfWeek() + " Hour " + getHourOfDay() + ": " + getNumberOfCommits() + " commits"; } } String getApiTailUrl(String tail) { return repo.getApiTailUrl("stats/" + tail); } }