Skip to content
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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/GitHubCodeReviewDashboard/GitHubCodeReviewDashboard.sln
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
111 changes: 111 additions & 0 deletions src/GitHubCodeReviewDashboard/GitHubCodeReviewDashboard/Dashboard.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using Octokit;
Copy link
Contributor

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?

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();
Copy link
Member

Choose a reason for hiding this comment

The 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);
}
Copy link
Member

Choose a reason for hiding this comment

The 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()
{
}
Copy link
Member

Choose a reason for hiding this comment

The 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";
}
28 changes: 28 additions & 0 deletions src/GitHubCodeReviewDashboard/GitHubCodeReviewDashboard/Program.cs
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>
Loading