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

Mass Issues Across all Repos in an org #90

Merged
merged 25 commits into from
Jun 23, 2020
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Diagnostics.CodeAnalysis;
using FluentAssertions;
using Konmaripo.Web.Controllers;
using Konmaripo.Web.Services;
using Moq;
using Serilog;
using Xunit;

namespace Konmaripo.Web.Tests.Unit.Controllers
{
public class MassIssuesControllerTests
{
[SuppressMessage("ReSharper", "ObjectCreationAsStatement")]
public class Ctor
{
private readonly Mock<ILogger> _mockLogger;
private readonly Mock<IGitHubService> _mockGitHubService;
private readonly Mock<IMassIssueCreator> _mockIssueCreator;

public Ctor()
{
_mockLogger = new Mock<ILogger>();
_mockGitHubService = new Mock<IGitHubService>();
_mockIssueCreator = new Mock<IMassIssueCreator>();
}

[Fact]
public void NullLogger_ThrowsException()
{
Action act = () => new MassIssuesController(null, _mockGitHubService.Object, _mockIssueCreator.Object);

act.Should().Throw<ArgumentNullException>()
.And.ParamName.Should().Be("logger");
}

[Fact]
public void NullGitHubService_ThrowsException()
{
Action act = () => new MassIssuesController(_mockLogger.Object, null, _mockIssueCreator.Object);

act.Should().Throw<ArgumentNullException>()
.And.ParamName.Should().Be("gitHubService");
}

[Fact]
public void NullMassIssueCreator_ThrowsException()
{
Action act = () => new MassIssuesController(_mockLogger.Object, _mockGitHubService.Object, null);

act.Should().Throw<ArgumentNullException>()
.And.ParamName.Should().Be("massIssueCreator");
}
}
}
}
129 changes: 129 additions & 0 deletions src/Konmaripo.Web/Controllers/MassIssuesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Konmaripo.Web.Services;
using Octokit;
using Serilog;

namespace Konmaripo.Web.Controllers
{
public class MassIssue
{
[Required]
[Display(Name = "Subject")]
public string IssueSubject { get; set; }

[Required]
[Display(Name = "Body")]
[DataType(DataType.MultilineText)]
public string IssueBody { get; set; }

[Display(Name = "Pin Issue?")]
public bool ShouldBePinned { get; set; }

// ReSharper disable once UnusedMember.Global
public MassIssue()
{
// this is here because the model binding uses it
}
public MassIssue(string issueSubject, string issueBody, bool shouldBePinned)
{
IssueSubject = issueSubject;
IssueBody = issueBody;
ShouldBePinned = shouldBePinned;
}
}
public class MassIssueViewModel
{
public MassIssue MassIssue { get; set; }
public int NonArchivedRepos { get; set; }
public int RemainingAPIRequests { get; set; }

// ReSharper disable once UnusedMember.Global
public MassIssueViewModel()
{
// This is here because the model binding uses it
}
public MassIssueViewModel(MassIssue massIssue, int nonArchivedRepos, int remainingApiRequests)
{
MassIssue = massIssue;
NonArchivedRepos = nonArchivedRepos;
RemainingAPIRequests = remainingApiRequests;
}
}

[Authorize]
public class MassIssuesController : Controller
{
private readonly ILogger _logger;
private readonly IGitHubService _gitHubService;
private readonly IMassIssueCreator _massIssueCreator;

public MassIssuesController(ILogger logger, IGitHubService gitHubService, IMassIssueCreator massIssueCreator)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_gitHubService = gitHubService ?? throw new ArgumentNullException(nameof(gitHubService));
_massIssueCreator = massIssueCreator ?? throw new ArgumentNullException(nameof(massIssueCreator));
}

public async Task<IActionResult> Index()
{
var remainingRequests = _gitHubService.RemainingAPIRequests();
var nonArchivedRepos = await NonArchivedReposCount();
var issue = new MassIssue(string.Empty, string.Empty, false);

var vm = new MassIssueViewModel(issue, nonArchivedRepos, remainingRequests);

return View(vm);
}

[HttpPost]
public async Task<IActionResult> Index(MassIssueViewModel vm)
{
var nonArchivedReposCount = await NonArchivedReposCount();
if (!ModelState.IsValid)
{
_logger.Warning("Mass issue model is invalid; returning validation error messages.");
vm.MassIssue.IssueSubject = vm.MassIssue.IssueSubject;
vm.MassIssue.IssueBody = vm.MassIssue.IssueBody;
vm.NonArchivedRepos = nonArchivedReposCount;
vm.RemainingAPIRequests = _gitHubService.RemainingAPIRequests();
return View(vm);
}

var currentUser = this.User.Identity.Name;
var newIssue = new NewIssue(vm.MassIssue.IssueSubject)
{
Body = @$"{vm.MassIssue.IssueBody}

----

Created by {currentUser} using the Konmaripo tool"
};

try
{
var repos = await _gitHubService.GetRepositoriesForOrganizationAsync();
var nonArchivedRepos = repos.Where(x => !x.IsArchived).ToList();
await Task.WhenAll(_massIssueCreator.CreateIssue(newIssue, nonArchivedRepos));
}
catch (Exception ex)
{
_logger.Error(ex, "An error occurred while creating the mass issues.");
}

return View("IssueSuccess");


}

private async Task<int> NonArchivedReposCount()
{
var repos = await _gitHubService.GetRepositoriesForOrganizationAsync();
return repos.Count(x => !x.IsArchived);
}
}
}
11 changes: 11 additions & 0 deletions src/Konmaripo.Web/Services/CachedGitHubService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Functional.Maybe;
using Konmaripo.Web.Models;
using Microsoft.Extensions.Caching.Memory;
using Octokit;

namespace Konmaripo.Web.Services
{
Expand Down Expand Up @@ -68,5 +69,15 @@ public Task<RepoQuota> GetRepoQuotaForOrg()
{
return _gitHubService.GetRepoQuotaForOrg();
}

public int RemainingAPIRequests()
{
return _gitHubService.RemainingAPIRequests();
}

public Task CreateIssueInRepo(NewIssue issue, long repoId)
{
return _gitHubService.CreateIssueInRepo(issue, repoId);
}
}
}
12 changes: 10 additions & 2 deletions src/Konmaripo.Web/Services/GitHubService.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Konmaripo.Web.Models;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.Extensions.Options;
using Octokit;
using Serilog;
Expand Down Expand Up @@ -80,5 +78,15 @@ public async Task<RepoQuota> GetRepoQuotaForOrg()
var org = await _githubClient.Organization.Get(_gitHubSettings.OrganizationName);
return new RepoQuota(org.Plan.PrivateRepos, org.OwnedPrivateRepos);
}

public int RemainingAPIRequests()
{
return _githubClient.GetLastApiInfo().RateLimit.Remaining;
}

public Task CreateIssueInRepo(NewIssue issue, long repoId)
{
return _githubClient.Issue.Create(repoId, issue);
}
}
}
3 changes: 3 additions & 0 deletions src/Konmaripo.Web/Services/IGitHubService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Konmaripo.Web.Models;
using Octokit;

namespace Konmaripo.Web.Services
{
Expand All @@ -11,5 +12,7 @@ public interface IGitHubService
Task CreateArchiveIssueInRepo(long repoId, string currentUser);
Task ArchiveRepository(long repoId, string repoName);
Task<RepoQuota> GetRepoQuotaForOrg();
int RemainingAPIRequests();
Task CreateIssueInRepo(NewIssue issue, long repoId);
}
}
12 changes: 12 additions & 0 deletions src/Konmaripo.Web/Services/IMassIssueCreator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Konmaripo.Web.Models;
using Octokit;

namespace Konmaripo.Web.Services
{
public interface IMassIssueCreator
{
List<Task> CreateIssue(NewIssue issue, List<GitHubRepo> repoList);
}
}
51 changes: 51 additions & 0 deletions src/Konmaripo.Web/Services/MassIssueCreator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Konmaripo.Web.Models;
using Octokit;
using Serilog;

namespace Konmaripo.Web.Services
{
public class MassIssueCreator : IMassIssueCreator
{
private readonly System.Threading.SemaphoreSlim _batcher = new System.Threading.SemaphoreSlim(10, 10);
private readonly IGitHubService _gitHubService;
private readonly ILogger _logger;

public MassIssueCreator(IGitHubService gitHubService, ILogger logger)
{
_gitHubService = gitHubService;
_logger = logger;
}

public List<Task> CreateIssue(NewIssue issue, List<GitHubRepo> repoList)
{
_logger.Information("Queuing issues for {RepoCount} repositories", repoList.Count);
var taskList = new List<Task>();

repoList.ForEach(x=> taskList.Add(CreateIssue(issue, x.Id)));

_logger.Information("Added tasks");

return taskList;
}
private async Task CreateIssue(NewIssue issue, long repoId)
{
await _batcher.WaitAsync();

try
{
await _gitHubService.CreateIssueInRepo(issue, repoId);
}
catch (Exception e)
{
_logger.Error(e, "An error occurred while creating an issue in repoId {RepoId}", repoId);
}
finally
{
_batcher.Release();
}
}
}
}
2 changes: 2 additions & 0 deletions src/Konmaripo.Web/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ public void ConfigureServices(IServiceCollection services)

return cachedService;
});

services.AddTransient<IMassIssueCreator, MassIssueCreator>();
}

/// <summary>
Expand Down
3 changes: 2 additions & 1 deletion src/Konmaripo.Web/Views/Home/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
<div class="text-center">
<h1 class="display-4">Welcome to Konmaripo!</h1>
<p>A tool to enable easier GitHub repo and organization management.</p>
<p>Currently, this tool enables you to @Html.ActionLink("sunset and archive repositories", "Index", "Sunsetting").</p>
<p>Currently, this tool enables you to @(Html.ActionLink("sunset and archive repositories", "Index", "MassIssues")).</p>
<p>You can also @Html.ActionLink("Submit a mass issue", "Index", "MassIssues").</p>
</div>

<div class="text-center">
Expand Down
Loading