Skip to content
This repository has been archived by the owner on Jul 12, 2022. It is now read-only.

Commit

Permalink
feat: Support specifying threshold for open PRs
Browse files Browse the repository at this point in the history
Before, NuKeeper would only generate a single pull request as it would
reselect the same updateset every single time, unless some outside
factors would influence the result of its prioritization algorithm.

Now, you can specify the max number of open pull requests on a
per-repository basis. This is currently only supported for Azure
Directory/TFS, however only Azure Directory Server has been tested.

Since there are no straightforward APIs for figuring out the number of
open pull requests, especially when using a PAT with `Code (Read &
Write)`, heuristics are used to figure out the number of open pull
requests as best as possible.

First, the current user is fetched. If this fails, a user by the name of
`nukeeper@bot.com` will be fetched. If this fails, all pull requests in
the repository will be considered that have the label `nukeeper`. If
none of these things were possible, it's assumed that there are 0 open
pull requests.

When the parameter `--consolidate` is specified, the default value for
`--maxopenpullrequests` is 1, otherwise it is `maxpackageupdates`.
  • Loading branch information
CrispyDrone committed Oct 10, 2020
1 parent 52f14d1 commit 4707677
Show file tree
Hide file tree
Showing 23 changed files with 807 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public void MissingFileReturnsNoSettings()
Assert.That(data.Exclude, Is.Null);
Assert.That(data.Label, Is.Null);
Assert.That(data.MaxPackageUpdates, Is.Null);
Assert.That(data.MaxOpenPullRequests, Is.Null);
Assert.That(data.MaxRepo, Is.Null);
Assert.That(data.Verbosity, Is.Null);
Assert.That(data.Change, Is.Null);
Expand Down Expand Up @@ -77,6 +78,7 @@ public void EmptyConfigReturnsNoSettings()
Assert.That(data.Exclude, Is.Null);
Assert.That(data.Label, Is.Null);
Assert.That(data.MaxPackageUpdates, Is.Null);
Assert.That(data.MaxOpenPullRequests, Is.Null);
Assert.That(data.MaxRepo, Is.Null);
Assert.That(data.Verbosity, Is.Null);
Assert.That(data.Change, Is.Null);
Expand All @@ -103,6 +105,7 @@ public void EmptyConfigReturnsNoSettings()
""logFile"":""somefile.log"",
""branchNameTemplate"": ""nukeeper/MyBranch"",
""maxPackageUpdates"": 42,
""maxOpenPullRequests"": 10,
""maxRepo"": 12,
""verbosity"": ""Detailed"",
""Change"": ""Minor"",
Expand Down Expand Up @@ -162,6 +165,7 @@ public void PopulatedConfigReturnsNumericSettings()
var data = fsr.Read(path);

Assert.That(data.MaxPackageUpdates, Is.EqualTo(42));
Assert.That(data.MaxOpenPullRequests, Is.EqualTo(10));
Assert.That(data.MaxRepo, Is.EqualTo(12));
}

Expand Down Expand Up @@ -197,6 +201,7 @@ public void ConfigKeysAreCaseInsensitive()
""IncluDeRepoS"":""repo2"",
""label"": [""mark"" ],
""MaxPackageUpdates"":4,
""MaxOpenPUllrequests"":10,
""MAXrepo"":3,
""vErBoSiTy"": ""Q"",
""CHANGE"": ""PATCH"",
Expand All @@ -219,6 +224,7 @@ public void ConfigKeysAreCaseInsensitive()
Assert.That(data.Label.Count, Is.EqualTo(1));
Assert.That(data.Label, Does.Contain("mark"));
Assert.That(data.MaxPackageUpdates, Is.EqualTo(4));
Assert.That(data.MaxOpenPullRequests, Is.EqualTo(10));
Assert.That(data.MaxRepo, Is.EqualTo(3));
Assert.That(data.Verbosity, Is.EqualTo(LogLevel.Quiet));
Assert.That(data.Change, Is.EqualTo(VersionChange.Patch));
Expand Down
2 changes: 2 additions & 0 deletions NuKeeper.Abstractions/CollaborationModels/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ namespace NuKeeper.Abstractions.CollaborationModels
{
public class User
{
public static readonly User Default = new User("user@email.com", "", "");

public User(string login, string name, string email)
{
Login = login;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ public interface ICollaborationPlatform
Task<bool> RepositoryBranchExists(string userName, string repositoryName, string branchName);

Task<SearchCodeResult> Search(SearchCodeRequest search);
Task<int> GetNumberOfOpenPullRequests(string projectName, string repositoryName);
}
}
1 change: 1 addition & 0 deletions NuKeeper.Abstractions/Configuration/FileSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public class FileSettings
public bool? DeleteBranchAfterMerge { get; set; }

public string GitCliPath { get; set; }
public int? MaxOpenPullRequests { get; set; }

public static FileSettings Empty()
{
Expand Down
1 change: 1 addition & 0 deletions NuKeeper.Abstractions/Configuration/UserSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public class UserSettings
public NuGetSources NuGetSources { get; set; }

public int MaxRepositoriesChanged { get; set; }
public int MaxOpenPullRequests { get; set; }

public bool ConsolidateUpdatesInSinglePullRequest { get; set; }

Expand Down
22 changes: 22 additions & 0 deletions NuKeeper.AzureDevOps/AzureDevOpsRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@ public static Uri BuildAzureDevOpsUri(string relativePath, bool previewApi = fal
: new Uri($"{relativePath}{separator}api-version=4.1", UriKind.Relative);
}

// documentation is confusing, I think this won't work without memberId or ownerId
// https://docs.microsoft.com/en-us/rest/api/azure/devops/account/accounts/list?view=azure-devops-rest-6.0
public Task<Resource<Account>> GetCurrentUser()
{
return GetResource<Resource<Account>>("/_apis/accounts");
}

public Task<Resource<Account>> GetUserByMail(string email)
{
var encodedEmail = HttpUtility.UrlEncode(email);
return GetResource<Resource<Account>>($"/_apis/identities?searchFilter=MailAddress&filterValue={encodedEmail}");
}

public async Task<IEnumerable<Project>> GetProjects()
{
var response = await GetResource<ProjectResource>("/_apis/projects");
Expand Down Expand Up @@ -136,6 +149,15 @@ public async Task<IEnumerable<GitRefs>> GetRepositoryRefs(string projectName, st
return response?.value.AsEnumerable();
}

public async Task<IEnumerable<PullRequest>> GetPullRequests(string projectName, string repositoryName, string user)
{
var response = await GetResource<PullRequestResource>(
$"{projectName}/_apis/git/repositories/{repositoryName}/pullrequests?searchCriteria.creatorId={user}"
);

return response?.value.AsEnumerable();
}

public async Task<PullRequest> CreatePullRequest(PRRequest request, string projectName, string azureRepositoryId)
{
var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");
Expand Down
77 changes: 75 additions & 2 deletions NuKeeper.AzureDevOps/AzureDevopsPlatform.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using NuKeeper.Abstractions;
using NuKeeper.Abstractions.CollaborationModels;
using NuKeeper.Abstractions.CollaborationPlatform;
using NuKeeper.Abstractions.Configuration;
Expand Down Expand Up @@ -32,9 +33,42 @@ public void Initialise(AuthSettings settings)
_client = new AzureDevOpsRestClient(_clientFactory, _logger, settings.Token, settings.ApiBase);
}

public Task<User> GetCurrentUser()
public async Task<User> GetCurrentUser()
{
return Task.FromResult(new User("user@email.com", "", ""));
try
{
var currentAccounts = await _client.GetCurrentUser();
var account = currentAccounts.value.FirstOrDefault();

if (account == null)
return User.Default;

return new User(account.accountId, account.accountName, account.Mail);

}
catch (NuKeeperException)
{
return User.Default;
}
}

public async Task<User> GetUserByMail(string email)
{
try
{
var currentAccounts = await _client.GetUserByMail(email);
var account = currentAccounts.value.FirstOrDefault();

if (account == null)
return User.Default;

return new User(account.accountId, account.accountName, account.Mail);

}
catch (NuKeeperException)
{
return User.Default;
}
}

public async Task<bool> PullRequestExists(ForkData target, string headBranch, string baseBranch)
Expand Down Expand Up @@ -167,5 +201,44 @@ public async Task<SearchCodeResult> Search(SearchCodeRequest searchRequest)

return new SearchCodeResult(totalCount);
}

public async Task<int> GetNumberOfOpenPullRequests(string projectName, string repositoryName)
{
var user = await GetCurrentUser();

if (user == User.Default)
{
// TODO: allow this to be configurable
user = await GetUserByMail("bot@nukeeper.com");
}

var prs = await _client.GetPullRequests(
projectName,
repositoryName,
user == User.Default ?
string.Empty
: user.Login
);

if (user == User.Default)
{
var relevantPrs = prs?
.Where(
pr => pr.labels
?.FirstOrDefault(
l => l.name.Equals(
"nukeeper",
StringComparison.InvariantCultureIgnoreCase
)
)?.active ?? false
);

return relevantPrs?.Count() ?? 0;
}
else
{
return prs?.Count() ?? 0;
}
}
}
}
47 changes: 47 additions & 0 deletions NuKeeper.AzureDevOps/AzureDevopsRestTypes.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;

Expand All @@ -7,6 +8,42 @@ namespace NuKeeper.AzureDevOps
#pragma warning disable CA1707 // Identifiers should not contain underscores
#pragma warning disable CA2227 // Collection properties should be read only

public class Resource<T>
{
public int count { get; set; }
public IEnumerable<T> value { get; set; }
}

public class Account
{
public string accountId { get; set; }
public string accountName { get; set; }
public string accountOwner { get; set; }
public Dictionary<string, object> properties { get; set; }
public string Mail
{
get
{
if (properties.ContainsKey("Mail"))
{
switch (properties["Mail"])
{
case JObject mailObject:
return mailObject.Property("$value").Value.ToString();

case JProperty mailProp:
return mailProp.Value.ToString();

case string mailString:
return mailString;
}
}

return string.Empty;
}
}
}

public class Avatar
{
public string href { get; set; }
Expand Down Expand Up @@ -74,13 +111,23 @@ public class PullRequest
public string MergeId { get; set; }
public string Url { get; set; }
public bool SupportsIterations { get; set; }
public IEnumerable<WebApiTagDefinition> labels { get; set; }

// public CreatedBy CreatedBy { get; set; }
// public Lastmergesourcecommit LastMergeSourceCommit { get; set; }
// public Lastmergetargetcommit LastMergeTargetCommit { get; set; }
// public Lastmergecommit LastMergeCommit { get; set; }
// public IEnumerable<Reviewer> Reviewers { get; set; }
}

public class WebApiTagDefinition
{
public bool active { get; set; }
public string id { get; set; }
public string name { get; set; }
public string url { get; set; }
}

public class ProjectResource
{
public int Count { get; set; }
Expand Down
5 changes: 5 additions & 0 deletions NuKeeper.BitBucket/BitbucketPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,10 @@ private static Repository MapRepository(BitBucket.Models.Repository repo)
new Uri(repo.links.html.href),
null, false, null);
}

public Task<int> GetNumberOfOpenPullRequests(string projectName, string repositoryName)
{
return Task.FromResult(0);
}
}
}
5 changes: 5 additions & 0 deletions NuKeeper.GitHub/OctokitClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -263,5 +263,10 @@ private static async Task<T> ExceptionHandler<T>(Func<Task<T>> funcToCheck)
throw new NuKeeperException(ex.Message, ex);
}
}

public Task<int> GetNumberOfOpenPullRequests(string projectName, string repositoryName)
{
return Task.FromResult(0);
}
}
}
5 changes: 5 additions & 0 deletions NuKeeper.Gitea/GiteaPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,10 @@ private Repository MapRepository(Gitea.Model.Repository repo)
repo.IsFork,
repo.Parent != null ? MapRepository(repo.Parent) : null);
}

public Task<int> GetNumberOfOpenPullRequests(string projectName, string repositoryName)
{
return Task.FromResult(0);
}
}
}
5 changes: 5 additions & 0 deletions NuKeeper.Gitlab/GitlabPlatform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,5 +126,10 @@ public Task<SearchCodeResult> Search(SearchCodeRequest search)
_logger.Error($"Search has not yet been implemented for GitLab.");
throw new NotImplementedException();
}

public Task<int> GetNumberOfOpenPullRequests(string projectName, string repositoryName)
{
return Task.FromResult(0);
}
}
}
Loading

0 comments on commit 4707677

Please sign in to comment.