-
Notifications
You must be signed in to change notification settings - Fork 76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Create a simple dashboard to view all pull requests by who should review them #774
base: main
Are you sure you want to change the base?
Changes from all commits
0326353
0fd6249
04abeda
f6befd8
ad6bbac
c0ab9d4
b0001db
4b46686
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
| ||
Microsoft Visual Studio Solution File, Format Version 12.00 | ||
# Visual Studio Version 16 | ||
VisualStudioVersion = 16.0.30108.6 | ||
MinimumVisualStudioVersion = 10.0.40219.1 | ||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitHubCodeReviewDashboard", "GitHubCodeReviewDashboard\GitHubCodeReviewDashboard.csproj", "{DA2E9AE8-2365-49FA-BD0B-DE7669A33C73}" | ||
EndProject | ||
Global | ||
GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||
Debug|Any CPU = Debug|Any CPU | ||
Release|Any CPU = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(ProjectConfigurationPlatforms) = postSolution | ||
{DA2E9AE8-2365-49FA-BD0B-DE7669A33C73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||
{DA2E9AE8-2365-49FA-BD0B-DE7669A33C73}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||
{DA2E9AE8-2365-49FA-BD0B-DE7669A33C73}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||
{DA2E9AE8-2365-49FA-BD0B-DE7669A33C73}.Release|Any CPU.Build.0 = Release|Any CPU | ||
EndGlobalSection | ||
GlobalSection(SolutionProperties) = preSolution | ||
HideSolutionNode = FALSE | ||
EndGlobalSection | ||
GlobalSection(ExtensibilityGlobals) = postSolution | ||
SolutionGuid = {6C4F1BFE-10E1-4F9A-A5B9-54A0E74C5756} | ||
EndGlobalSection | ||
EndGlobal |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
using Octokit; | ||
using Octokit.Caching; | ||
using Octokit.Internal; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace GitHubCodeReviewDashboard | ||
{ | ||
public static class Dashboard | ||
{ | ||
public static async Task<(ImmutableDictionary<string, ImmutableArray<PullRequest>>, RequestCounter)> GetCategorizedPullRequests() | ||
{ | ||
var github = new GitHubClient(new ProductHeaderValue("dotnet-roslyn-Code-Review-Dashboard")); | ||
var requestCounter = new RequestCounter(); | ||
github.Credentials = new Credentials(Startup.GitHubToken); | ||
github.ResponseCache = requestCounter; | ||
var openPullRequests = await GetAllPullRequests(github); | ||
var ideTeamMembers = (await github.Organization.Team.GetAllMembers(1781706)).Select(u => u.Login).ToList(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. constant? |
||
|
||
var pullRequestsByCategory = new Dictionary<string, ImmutableArray<PullRequest>.Builder>(); | ||
|
||
void AddToCategory(string category, PullRequest pullRequest) | ||
{ | ||
if (!pullRequestsByCategory.TryGetValue(category, out var pullRequests)) | ||
{ | ||
pullRequests = ImmutableArray.CreateBuilder<PullRequest>(); | ||
pullRequestsByCategory.Add(category, pullRequests); | ||
} | ||
|
||
pullRequests.Add(pullRequest); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move to bottom |
||
|
||
foreach (var openPullRequest in openPullRequests) | ||
{ | ||
var requestedReviewers = openPullRequest.RequestedReviewers.Where(a => ideTeamMembers.Contains(a.Login)) | ||
.Select(a => a.Login).ToList(); | ||
|
||
// For assignees, exclude self-assignment since that's not terribly useful to show. | ||
var assignees = openPullRequest.Assignees.Where(a => ideTeamMembers.Contains(a.Login) && | ||
a.Id != openPullRequest.User.Id) | ||
.Select(a => a.Login).ToList(); | ||
|
||
// We will exclude PRs created by dotnet-bot since those are usually auto-merges, but if it's | ||
// assigned to somebody then we'll still show that since that might mean the merge resolution needs | ||
// something done with it. | ||
if (openPullRequest.RequestedTeams.Any(t => t.Name == "roslyn-ide") && | ||
!requestedReviewers.Any() && | ||
!assignees.Any() && | ||
!openPullRequest.Draft && | ||
openPullRequest.User.Login != "dotnet-bot") | ||
{ | ||
AddToCategory("(untriaged)", openPullRequest); | ||
} | ||
else | ||
{ | ||
// If the PR is a draft PR, we'll only show explicit requests, otherwise requests plus assignees | ||
// since for community members people we assign the PR to are on the hook for reviewing as well. | ||
var responsibleUsers = openPullRequest.Draft ? requestedReviewers : requestedReviewers.Concat(assignees).Distinct(); | ||
|
||
foreach (var responsibleUser in responsibleUsers) | ||
{ | ||
AddToCategory(responsibleUser, openPullRequest); | ||
} | ||
} | ||
} | ||
|
||
return (ImmutableDictionary.CreateRange( | ||
pullRequestsByCategory.Select(kvp => KeyValuePair.Create(kvp.Key, kvp.Value.ToImmutable()))), requestCounter); | ||
} | ||
|
||
private static async Task<ImmutableArray<PullRequest>> GetAllPullRequests(GitHubClient github) | ||
{ | ||
(string org, string name)[] repositories = [ | ||
("dotnet", "format"), | ||
("dotnet", "roslyn"), | ||
("dotnet", "roslyn-analyzers"), | ||
("dotnet", "roslyn-sdk"), | ||
("dotnet", "roslyn-tools"), | ||
("dotnet", "vscode-csharp"), | ||
]; | ||
|
||
var tasks = repositories.Select(repo => github.PullRequest.GetAllForRepository(repo.org, repo.name, new ApiOptions { PageSize = 100 })); | ||
var allPullRequests = await Task.WhenAll(tasks); | ||
|
||
return allPullRequests.SelectMany(prs => prs).OrderByDescending(pr => pr.CreatedAt).ToImmutableArray(); | ||
} | ||
|
||
/// <summary> | ||
/// This is an implementation of <see cref="IResponseCache"/> that doesn't actually cache anything, but counts the number of requests we do | ||
/// to make sure we're not doing something terrible. | ||
/// </summary> | ||
public class RequestCounter : IResponseCache | ||
{ | ||
public int RequestCount; | ||
|
||
public Task<CachedResponse.V1> GetAsync(IRequest request) | ||
{ | ||
Interlocked.Increment(ref RequestCount); | ||
return Task.FromResult<CachedResponse.V1>(null); | ||
} | ||
|
||
public Task SetAsync(IRequest request, CachedResponse.V1 cachedResponse) | ||
{ | ||
return Task.CompletedTask; | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>net7.0</TargetFramework> | ||
<UserSecretsId>code-review-dashboard-c8280495-0f86-4936-ace1-48033adf44da</UserSecretsId> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Humanizer" Version="2.14.1" /> | ||
<PackageReference Include="Octokit" Version="9.1.0" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<Folder Include="Properties\PublishProfiles\" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
@page | ||
@model ErrorModel | ||
@{ | ||
ViewData["Title"] = "Error"; | ||
} | ||
|
||
<h1 class="text-danger">Error.</h1> | ||
<h2 class="text-danger">An error occurred while processing your request.</h2> | ||
|
||
@if (Model.ShowRequestId) | ||
{ | ||
<p> | ||
<strong>Request ID:</strong> <code>@Model.RequestId</code> | ||
</p> | ||
} | ||
|
||
<h3>Development Mode</h3> | ||
<p> | ||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred. | ||
</p> | ||
<p> | ||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong> | ||
It can result in displaying sensitive information from exceptions to end users. | ||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong> | ||
and restarting the app. | ||
</p> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.AspNetCore.Mvc.RazorPages; | ||
using Microsoft.Extensions.Logging; | ||
using System.Diagnostics; | ||
|
||
namespace GitHubCodeReviewDashboard.Pages | ||
{ | ||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] | ||
public class ErrorModel : PageModel | ||
{ | ||
public string RequestId { get; set; } | ||
|
||
public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); | ||
|
||
private readonly ILogger<ErrorModel> _logger; | ||
|
||
public ErrorModel(ILogger<ErrorModel> logger) | ||
{ | ||
_logger = logger; | ||
} | ||
|
||
public void OnGet() | ||
{ | ||
RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
@page | ||
@model IndexModel | ||
@{ | ||
ViewData["Title"] = "Active Pull Requests"; | ||
} | ||
|
||
@{ | ||
var (categorizedPullRequests, requestCounter) = await Dashboard.GetCategorizedPullRequests(); | ||
|
||
} | ||
@foreach (var (category, pullRequests) in categorizedPullRequests.OrderBy(i => i.Key)) | ||
{ | ||
@if (string.IsNullOrEmpty(Model.Categories) || Model.Categories.Split(",").Contains(category)) | ||
{ | ||
<h2 class="category">@category (@pullRequests.Length) <a href="?categories=@category">(filter to just this)</a></h2> | ||
<ul> | ||
|
||
@foreach (var pullRequest in pullRequests) | ||
{ | ||
<li> | ||
@if (pullRequest.Draft) | ||
{ | ||
<span class="draft-label">Draft</span> | ||
} | ||
|
||
<a href="@pullRequest.HtmlUrl">@pullRequest.Title</a> by @pullRequest.User.Login @pullRequest.CreatedAt.Humanize() in @pullRequest.Base.Repository.FullName. | ||
</li> | ||
} | ||
|
||
</ul> | ||
} | ||
} | ||
<p><i>Generation of this page performed @("request".ToQuantity(requestCounter.RequestCount)) to the GitHub API.</i></p> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
using Microsoft.AspNetCore.Mvc; | ||
using Microsoft.AspNetCore.Mvc.RazorPages; | ||
|
||
namespace GitHubCodeReviewDashboard.Pages | ||
{ | ||
public class IndexModel : PageModel | ||
{ | ||
[BindProperty(SupportsGet = true)] | ||
public string Categories { get; set; } | ||
|
||
public void OnGet() | ||
{ | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="utf-8" /> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
<title>@ViewData["Title"] - Code Review Dashboard</title> | ||
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> | ||
</head> | ||
<body> | ||
<div class="container"> | ||
<main role="main" class="pb-3"> | ||
@RenderBody() | ||
</main> | ||
</div> | ||
|
||
<script src="~/js/site.js" asp-append-version="true"></script> | ||
|
||
@RenderSection("Scripts", required: false) | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
@using Humanizer | ||
@using GitHubCodeReviewDashboard | ||
@namespace GitHubCodeReviewDashboard.Pages | ||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
@{ | ||
Layout = "_Layout"; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
using Microsoft.AspNetCore.Hosting; | ||
using Microsoft.Extensions.Configuration; | ||
using Microsoft.Extensions.Hosting; | ||
|
||
namespace GitHubCodeReviewDashboard | ||
{ | ||
public class Program | ||
{ | ||
public static void Main(string[] args) | ||
{ | ||
CreateHostBuilder(args).Build().Run(); | ||
} | ||
|
||
public static IHostBuilder CreateHostBuilder(string[] args) => | ||
Host.CreateDefaultBuilder(args) | ||
.ConfigureAppConfiguration((hostContext, configurationBuilder) => | ||
{ | ||
if (hostContext.HostingEnvironment.IsDevelopment()) | ||
{ | ||
configurationBuilder.AddUserSecrets<Program>(); | ||
} | ||
}) | ||
.ConfigureWebHostDefaults(webBuilder => | ||
{ | ||
webBuilder.UseStartup<Startup>(); | ||
}); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<!-- | ||
This file is used by the publish/package process of your Web project. You can customize the behavior of this process | ||
by editing this MSBuild file. In order to learn more about this please visit https://go.microsoft.com/fwlink/?LinkID=208121. | ||
--> | ||
<Project> | ||
<PropertyGroup> | ||
<WebPublishMethod>ZipDeploy</WebPublishMethod> | ||
<IsLinux>true</IsLinux> | ||
<ResourceId>/subscriptions/f520f29d-58f8-4c73-bdb7-6d5dce8ce569/resourceGroups/roslyncodereviewdashboard/providers/Microsoft.Web/sites/roslyncodereviewdashboard</ResourceId> | ||
<ResourceGroup>roslyncodereviewdashboard</ResourceGroup> | ||
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish> | ||
<SiteUrlToLaunchAfterPublish>https://roslyncodereviewdashboard.azurewebsites.net</SiteUrlToLaunchAfterPublish> | ||
<PublishProvider>AzureWebSite</PublishProvider> | ||
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration> | ||
<LastUsedPlatform>Any CPU</LastUsedPlatform> | ||
<ProjectGuid>da2e9ae8-2365-49fa-bd0b-de7669a33c73</ProjectGuid> | ||
<PublishUrl>https://roslyncodereviewdashboard.scm.azurewebsites.net/</PublishUrl> | ||
<UserName>$roslyncodereviewdashboard</UserName> | ||
<_SavePWD>true</_SavePWD> | ||
</PropertyGroup> | ||
</Project> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need license headers for this repo?