Skip to content

Commit

Permalink
WIP: Moved issue queries to GraphQL
Browse files Browse the repository at this point in the history
Right now, is a bug preventing us from using this in production though:

    octokit/octokit.graphql.net#234

There is another one that might bite us too:

    octokit/octokit.graphql.net#227
  • Loading branch information
terrajobst committed Jul 6, 2020
1 parent cfe442b commit f756e1f
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 144 deletions.
1 change: 1 addition & 0 deletions ApiReview.Server.Logic/ApiReview.Server.Logic.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.38.2.1565" />
<PackageReference Include="Markdig" Version="0.17.1" />
<PackageReference Include="Octokit" Version="0.34.0" />
<PackageReference Include="Octokit.GraphQL" Version="0.1.6-beta" />
</ItemGroup>

<ItemGroup>
Expand Down
34 changes: 9 additions & 25 deletions ApiReview.Server.Logic/GitHubClientFactory.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.Threading.Tasks;
using Octokit;

using Octokit;
using QConnection = Octokit.GraphQL.Connection;
using QProductHeaderValue = Octokit.GraphQL.ProductHeaderValue;

namespace ApiReview.Server.Logic
{
Expand All @@ -10,36 +10,20 @@ internal static class GitHubClientFactory
public static GitHubClient Create()
{
var key = ApiKeyStore.GetApiKey();
return Create(key);
}

public static GitHubClient Create(string apiKey)
{
var productInformation = new ProductHeaderValue("APIReviewList");
var client = new GitHubClient(productInformation)
{
Credentials = new Credentials(apiKey)
Credentials = new Credentials(key)
};
return client;
}

public static async Task<bool> IsValidKeyAsync(string apiKey)
public static QConnection CreateGraph()
{
try
{
var client = Create(apiKey);
var request = new IssueRequest
{
Since = DateTimeOffset.Now
};
await client.Issue.GetAllForCurrent(request);
}
catch (Exception)
{
return false;
}

return true;
var key = ApiKeyStore.GetApiKey();
var productInformation = new QProductHeaderValue("APIReviewList");
var connection = new QConnection(productInformation, key);
return connection;
}
}
}
255 changes: 188 additions & 67 deletions ApiReview.Server.Logic/GitHubManager.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading.Tasks;

using ApiReview.Shared;

using Octokit;
using Octokit.GraphQL;
using Octokit.GraphQL.Model;

using Issue = Octokit.Issue;
using static Octokit.GraphQL.Variable;

namespace ApiReview.Server.Logic
{
Expand Down Expand Up @@ -53,12 +59,12 @@ public Task<IReadOnlyList<ApiReviewFeedback>> GetFeedbackAsync(DateTimeOffset st

private static async Task<IReadOnlyList<ApiReviewFeedback>> GetFeedbackAsync(OrgAndRepo[] repos, DateTimeOffset start, DateTimeOffset end)
{
static string GetApiStatus(Issue issue)
static string GetApiStatus(FeedbackIssue issue)
{
var isReadyForReview = issue.Labels.Any(l => l.Name == "api-ready-for-review");
var isApproved = issue.Labels.Any(l => l.Name == "api-approved");
var needsWork = issue.Labels.Any(l => l.Name == "api-needs-work");
var isRejected = isReadyForReview && issue.State.Value == ItemState.Closed;
var isRejected = isReadyForReview && issue.State == IssueState.Open;

var isApi = isApproved || needsWork || isRejected;

Expand All @@ -74,47 +80,46 @@ static string GetApiStatus(Issue issue)
return "Needs Work";
}

static bool WasEverReadyForReview(Issue issue, IEnumerable<EventInfo> events)
static bool WasEverReadyForReview(FeedbackIssue issue)
{
if (issue.Labels.Any(l => l.Name == "api-ready-for-review" ||
l.Name == "api-approved"))
return true;

foreach (var eventInfo in events)
foreach (var timelineItem in issue.TimelineItems)
{
if (eventInfo.Label?.Name == "api-ready-for-review" ||
eventInfo.Label?.Name == "api-approved")
return true;
if (timelineItem is ApiTimelineLabel labelItem)
{
if (labelItem.Name == "api-ready-for-review" ||
labelItem.Name == "api-approved")
return true;
}
}

return false;
}

static bool IsApiEvent(EventInfo eventInfo)
static bool IsApiEvent(ApiTimelineItem item)
{
// We need to work around unsupported enum values:
// - https://github.com/octokit/octokit.net/issues/2023
// - https://github.com/octokit/octokit.net/issues/2025
//
// which will cause Value to throw an exception.

switch (eventInfo.Event.StringValue)
if (item is ApiTimelineLabel l)
{
case "labeled":
if (eventInfo.Label.Name == "api-approved" || eventInfo.Label.Name == "api-needs-work")
return true;
break;
case "closed":
if (l.Name == "api-approved" || l.Name == "api-needs-work")
return true;
}
else if (item is ApiTimelineClosure c)
{
return true;
}

return false;
}

static IEnumerable<EventInfo> GetApiEvents(IEnumerable<EventInfo> events, DateTimeOffset start, DateTimeOffset end)
static IEnumerable<ApiTimelineItem> GetApiEvents(FeedbackIssue issue, DateTimeOffset start, DateTimeOffset end)
{
foreach (var eventGroup in events.Where(e => start <= e.CreatedAt && e.CreatedAt <= end && IsApiEvent(e))
.GroupBy(e => e.CreatedAt.Date))
foreach (var eventGroup in issue.TimelineItems
.Where(e => e != null)
.Where(e => start <= e.CreatedAt && e.CreatedAt <= end && IsApiEvent(e))
.GroupBy(e => e.CreatedAt.Date))
{
var latest = eventGroup.OrderBy(e => e.CreatedAt).Last();
yield return latest;
Expand Down Expand Up @@ -143,60 +148,133 @@ static IEnumerable<EventInfo> GetApiEvents(IEnumerable<EventInfo> events, DateTi
return (null, body);
}

var github = GitHubClientFactory.Create();
var results = new List<ApiReviewFeedback>();

foreach (var (owner, repo) in repos)
static ApiReviewIssue CreateIssue(FeedbackIssue issue)
{
var request = new RepositoryIssueRequest
var result = new ApiReviewIssue
{
Filter = IssueFilter.All,
State = ItemStateFilter.All,
Since = start
Owner = issue.Owner,
Repo = issue.Repo,
Author = issue.Author,
CreatedAt = issue.CreateAt,
Labels = issue.Labels.ToArray(),
//Milestone = issue.Milestone ?? "(None)",
Title = GitHubIssueHelpers.FixTitle(issue.Title),
Url = issue.Url,
Id = issue.Number
};
return result;
}

var issues = await github.Issue.GetAllForRepository(owner, repo, request);
var filter = new IssueFilters()
{
Assignee = "*",
Milestone = "*",
Since = start,
};
var query = new Query()
.Repository(Var("repo"), Var("owner"))
.Issues(filterBy: filter)
.AllPages()
.Select(i => new FeedbackIssue
{
Owner = i.Repository.Owner.Login,
Repo = i.Repository.Name,
Number = i.Number,
Title = i.Title,
CreateAt = i.CreatedAt,
Author = i.Author.Login,
State = i.State,
//Milestone = i.Milestone.Title,
Url = i.Url,
Labels = i.Labels(null, null, null, null, null)
.AllPages()
.Select(l => new ApiReviewLabel
{
Name = l.Name,
BackgroundColor = l.Color
}).ToList(),
TimelineItems = i
.TimelineItems(null, null, null, null, null, start, null)
.AllPages()
.Select(tl => tl == null ? null : tl.Switch<ApiTimelineItem>(when =>
when.IssueComment(ic => new ApiTimelineComment
{
Id = ic.Id.Value,
Body = ic.Body,
Url = ic.Url,
Actor = ic.Author.Login,
CreatedAt = ic.CreatedAt
}).LabeledEvent(l => new ApiTimelineLabel
{
Name = l.Label.Name,
Actor = l.Actor.Login,
CreatedAt = l.CreatedAt
}).ClosedEvent(c => new ApiTimelineClosure
{
Actor = c.Actor.Login,
CreatedAt = c.CreatedAt
}))).ToList()
}).Compile();

foreach (var issue in issues)
var connection = GitHubClientFactory.CreateGraph();
var vars = new Dictionary<string, object>();

var issues = new List<FeedbackIssue>();

foreach (var ownerAndRepo in repos)
{
vars["owner"] = ownerAndRepo.OrgName;
vars["repo"] = ownerAndRepo.RepoName;
try
{
var status = GetApiStatus(issue);
if (status == null)
continue;
var current = await connection.Run(query, vars);
issues.AddRange(current);
}
catch (Exception ex)
{
throw;
}
}

var events = await github.Issue.Events.GetAllForIssue(owner, repo, issue.Number);
var results = new List<ApiReviewFeedback>();

foreach (var issue in issues)
{
var status = GetApiStatus(issue);
if (status == null)
continue;

if (!WasEverReadyForReview(issue, events))
continue;
if (!WasEverReadyForReview(issue))
continue;

foreach (var apiEvent in GetApiEvents(events, start, end))
foreach (var apiEvent in GetApiEvents(issue, start, end))
{
var title = GitHubIssueHelpers.FixTitle(issue.Title);
var feedbackDateTime = apiEvent.CreatedAt;
var comments = issue.TimelineItems.OfType<ApiTimelineComment>();
var eventComment = comments.Where(c => c.Actor == apiEvent.Actor)
.Select(c => (comment: c, within: Math.Abs((c.CreatedAt - feedbackDateTime).TotalSeconds)))
.Where(c => c.within <= TimeSpan.FromMinutes(15).TotalSeconds)
.OrderBy(c => c.within)
.Select(c => c.comment)
.FirstOrDefault();
var feedbackId = eventComment?.Id;
var feedbackUrl = eventComment?.Url ?? issue.Url;
var (videoUrl, feedbackMarkdown) = ParseFeedback(eventComment?.Body);

var apiReviewIssue = CreateIssue(issue);

var feedback = new ApiReviewFeedback
{
var title = GitHubIssueHelpers.FixTitle(issue.Title);
var feedbackDateTime = apiEvent.CreatedAt;
var comments = await github.Issue.Comment.GetAllForIssue(owner, repo, issue.Number);
var eventComment = comments.Where(c => c.User.Login == apiEvent.Actor.Login)
.Select(c => (comment: c, within: Math.Abs((c.CreatedAt - feedbackDateTime).TotalSeconds)))
.Where(c => c.within <= TimeSpan.FromMinutes(15).TotalSeconds)
.OrderBy(c => c.within)
.Select(c => c.comment)
.FirstOrDefault();
var feedbackId = eventComment?.Id;
var feedbackUrl = eventComment?.HtmlUrl ?? issue.HtmlUrl;
var (videoUrl, feedbackMarkdown) = ParseFeedback(eventComment?.Body);

var apiReviewIssue = CreateIssue(owner, repo, issue);

var feedback = new ApiReviewFeedback
{
Issue = apiReviewIssue,
FeedbackId = feedbackId,
FeedbackDateTime = feedbackDateTime,
FeedbackUrl = feedbackUrl,
FeedbackStatus = status,
FeedbackMarkdown = feedbackMarkdown,
VideoUrl = videoUrl
};
results.Add(feedback);
}
Issue = apiReviewIssue,
FeedbackId = feedbackId,
FeedbackDateTime = feedbackDateTime,
FeedbackUrl = feedbackUrl,
FeedbackStatus = status,
FeedbackMarkdown = feedbackMarkdown,
VideoUrl = videoUrl
};
results.Add(feedback);
}
}

Expand Down Expand Up @@ -250,5 +328,48 @@ private static ApiReviewIssue CreateIssue(string owner, string repo, Issue issue
};
return result;
}

private sealed class FeedbackIssue
{
public string Owner { get; set; }
public string Repo { get; set; }
public int Number { get; set; }
public DateTimeOffset CreateAt { get; set; }
public string Author { get; set; }
public string Title { get; set; }
public IssueState State { get; set; }
public string Milestone { get; set; }
public string Url { get; set; }
public List<ApiReviewLabel> Labels { get; set; }
public List<ApiTimelineItem> TimelineItems { get; set; }

public override string ToString()
{
return $"{Owner}/{Repo}#{Number}: {Title}";
}
}

private abstract class ApiTimelineItem
{
public string Actor { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}

private sealed class ApiTimelineComment : ApiTimelineItem
{
public string Id { get; set; }
public string Body { get; set; }
public string Url { get; set; }
}

private sealed class ApiTimelineLabel : ApiTimelineItem
{
public string Name { get; set; }
public ApiReviewLabel Label { get; set; }
}

private sealed class ApiTimelineClosure : ApiTimelineItem
{
}
}
}

0 comments on commit f756e1f

Please sign in to comment.