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

Commit 18d6cdb

Browse files
authored
feat: Support specifying threshold for open PRs (#1043)
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 Devops/TFS, however only Azure Devops 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 `[email protected]` 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`.
1 parent 4c71eeb commit 18d6cdb

File tree

23 files changed

+823
-38
lines changed

23 files changed

+823
-38
lines changed

NuKeeper.Abstractions.Tests/Configuration/FileSettingsReaderTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public void MissingFileReturnsNoSettings()
4646
Assert.That(data.Exclude, Is.Null);
4747
Assert.That(data.Label, Is.Null);
4848
Assert.That(data.MaxPackageUpdates, Is.Null);
49+
Assert.That(data.MaxOpenPullRequests, Is.Null);
4950
Assert.That(data.MaxRepo, Is.Null);
5051
Assert.That(data.Verbosity, Is.Null);
5152
Assert.That(data.Change, Is.Null);
@@ -77,6 +78,7 @@ public void EmptyConfigReturnsNoSettings()
7778
Assert.That(data.Exclude, Is.Null);
7879
Assert.That(data.Label, Is.Null);
7980
Assert.That(data.MaxPackageUpdates, Is.Null);
81+
Assert.That(data.MaxOpenPullRequests, Is.Null);
8082
Assert.That(data.MaxRepo, Is.Null);
8183
Assert.That(data.Verbosity, Is.Null);
8284
Assert.That(data.Change, Is.Null);
@@ -103,6 +105,7 @@ public void EmptyConfigReturnsNoSettings()
103105
""logFile"":""somefile.log"",
104106
""branchNameTemplate"": ""nukeeper/MyBranch"",
105107
""maxPackageUpdates"": 42,
108+
""maxOpenPullRequests"": 10,
106109
""maxRepo"": 12,
107110
""verbosity"": ""Detailed"",
108111
""Change"": ""Minor"",
@@ -162,6 +165,7 @@ public void PopulatedConfigReturnsNumericSettings()
162165
var data = fsr.Read(path);
163166

164167
Assert.That(data.MaxPackageUpdates, Is.EqualTo(42));
168+
Assert.That(data.MaxOpenPullRequests, Is.EqualTo(10));
165169
Assert.That(data.MaxRepo, Is.EqualTo(12));
166170
}
167171

@@ -197,6 +201,7 @@ public void ConfigKeysAreCaseInsensitive()
197201
""IncluDeRepoS"":""repo2"",
198202
""label"": [""mark"" ],
199203
""MaxPackageUpdates"":4,
204+
""MaxOpenPUllrequests"":10,
200205
""MAXrepo"":3,
201206
""vErBoSiTy"": ""Q"",
202207
""CHANGE"": ""PATCH"",
@@ -219,6 +224,7 @@ public void ConfigKeysAreCaseInsensitive()
219224
Assert.That(data.Label.Count, Is.EqualTo(1));
220225
Assert.That(data.Label, Does.Contain("mark"));
221226
Assert.That(data.MaxPackageUpdates, Is.EqualTo(4));
227+
Assert.That(data.MaxOpenPullRequests, Is.EqualTo(10));
222228
Assert.That(data.MaxRepo, Is.EqualTo(3));
223229
Assert.That(data.Verbosity, Is.EqualTo(LogLevel.Quiet));
224230
Assert.That(data.Change, Is.EqualTo(VersionChange.Patch));

NuKeeper.Abstractions/CollaborationModels/User.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ namespace NuKeeper.Abstractions.CollaborationModels
22
{
33
public class User
44
{
5+
public static readonly User Default = new User("[email protected]", "", "");
6+
57
public User(string login, string name, string email)
68
{
79
Login = login;

NuKeeper.Abstractions/CollaborationPlatform/ICollaborationPlatform.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ public interface ICollaborationPlatform
2626
Task<bool> RepositoryBranchExists(string userName, string repositoryName, string branchName);
2727

2828
Task<SearchCodeResult> Search(SearchCodeRequest search);
29+
Task<int> GetNumberOfOpenPullRequests(string projectName, string repositoryName);
2930
}
3031
}

NuKeeper.Abstractions/Configuration/FileSettings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public class FileSettings
4343
public bool? DeleteBranchAfterMerge { get; set; }
4444

4545
public string GitCliPath { get; set; }
46+
public int? MaxOpenPullRequests { get; set; }
4647

4748
public static FileSettings Empty()
4849
{

NuKeeper.Abstractions/Configuration/UserSettings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public class UserSettings
88
public NuGetSources NuGetSources { get; set; }
99

1010
public int MaxRepositoriesChanged { get; set; }
11+
public int MaxOpenPullRequests { get; set; }
1112

1213
public bool ConsolidateUpdatesInSinglePullRequest { get; set; }
1314

NuKeeper.AzureDevOps/AzureDevOpsRestClient.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,19 @@ public static Uri BuildAzureDevOpsUri(string relativePath, bool previewApi = fal
115115
: new Uri($"{relativePath}{separator}api-version=4.1", UriKind.Relative);
116116
}
117117

118+
// documentation is confusing, I think this won't work without memberId or ownerId
119+
// https://docs.microsoft.com/en-us/rest/api/azure/devops/account/accounts/list?view=azure-devops-rest-6.0
120+
public Task<Resource<Account>> GetCurrentUser()
121+
{
122+
return GetResource<Resource<Account>>("/_apis/accounts");
123+
}
124+
125+
public Task<Resource<Account>> GetUserByMail(string email)
126+
{
127+
var encodedEmail = HttpUtility.UrlEncode(email);
128+
return GetResource<Resource<Account>>($"/_apis/identities?searchFilter=MailAddress&filterValue={encodedEmail}");
129+
}
130+
118131
public async Task<IEnumerable<Project>> GetProjects()
119132
{
120133
var response = await GetResource<ProjectResource>("/_apis/projects");
@@ -148,6 +161,15 @@ public async Task<IEnumerable<PullRequest>> GetPullRequests(
148161
return response?.value.AsEnumerable();
149162
}
150163

164+
public async Task<IEnumerable<PullRequest>> GetPullRequests(string projectName, string repositoryName, string user)
165+
{
166+
var response = await GetResource<PullRequestResource>(
167+
$"{projectName}/_apis/git/repositories/{repositoryName}/pullrequests?searchCriteria.creatorId={user}"
168+
);
169+
170+
return response?.value.AsEnumerable();
171+
}
172+
151173
public async Task<PullRequest> CreatePullRequest(PRRequest request, string projectName, string azureRepositoryId)
152174
{
153175
var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, "application/json");

NuKeeper.AzureDevOps/AzureDevopsPlatform.cs

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using NuKeeper.Abstractions;
12
using NuKeeper.Abstractions.CollaborationModels;
23
using NuKeeper.Abstractions.CollaborationPlatform;
34
using NuKeeper.Abstractions.Configuration;
@@ -32,9 +33,42 @@ public void Initialise(AuthSettings settings)
3233
_client = new AzureDevOpsRestClient(_clientFactory, _logger, settings.Token, settings.ApiBase);
3334
}
3435

35-
public Task<User> GetCurrentUser()
36+
public async Task<User> GetCurrentUser()
3637
{
37-
return Task.FromResult(new User("[email protected]", "", ""));
38+
try
39+
{
40+
var currentAccounts = await _client.GetCurrentUser();
41+
var account = currentAccounts.value.FirstOrDefault();
42+
43+
if (account == null)
44+
return User.Default;
45+
46+
return new User(account.accountId, account.accountName, account.Mail);
47+
48+
}
49+
catch (NuKeeperException)
50+
{
51+
return User.Default;
52+
}
53+
}
54+
55+
public async Task<User> GetUserByMail(string email)
56+
{
57+
try
58+
{
59+
var currentAccounts = await _client.GetUserByMail(email);
60+
var account = currentAccounts.value.FirstOrDefault();
61+
62+
if (account == null)
63+
return User.Default;
64+
65+
return new User(account.accountId, account.accountName, account.Mail);
66+
67+
}
68+
catch (NuKeeperException)
69+
{
70+
return User.Default;
71+
}
3872
}
3973

4074
public async Task<bool> PullRequestExists(ForkData target, string headBranch, string baseBranch)
@@ -180,5 +214,58 @@ public async Task<SearchCodeResult> Search(SearchCodeRequest searchRequest)
180214

181215
return new SearchCodeResult(totalCount);
182216
}
217+
218+
public async Task<int> GetNumberOfOpenPullRequests(string projectName, string repositoryName)
219+
{
220+
var user = await GetCurrentUser();
221+
222+
if (user == User.Default)
223+
{
224+
// TODO: allow this to be configurable
225+
user = await GetUserByMail("[email protected]");
226+
}
227+
228+
var prs = await GetPullRequestsForUser(
229+
projectName,
230+
repositoryName,
231+
user == User.Default ?
232+
string.Empty
233+
: user.Login
234+
);
235+
236+
if (user == User.Default)
237+
{
238+
var relevantPrs = prs?
239+
.Where(
240+
pr => pr.labels
241+
?.FirstOrDefault(
242+
l => l.name.Equals(
243+
"nukeeper",
244+
StringComparison.InvariantCultureIgnoreCase
245+
)
246+
)?.active ?? false
247+
);
248+
249+
return relevantPrs?.Count() ?? 0;
250+
}
251+
else
252+
{
253+
return prs?.Count() ?? 0;
254+
}
255+
}
256+
257+
private async Task<IEnumerable<PullRequest>> GetPullRequestsForUser(string projectName, string repositoryName, string userName)
258+
{
259+
try
260+
{
261+
return await _client.GetPullRequests(projectName, repositoryName, userName);
262+
263+
}
264+
catch (NuKeeperException ex)
265+
{
266+
_logger.Error($"Failed to get pull requests for name {userName}", ex);
267+
return Enumerable.Empty<PullRequest>();
268+
}
269+
}
183270
}
184271
}

NuKeeper.AzureDevOps/AzureDevopsRestTypes.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Newtonsoft.Json.Linq;
12
using System;
23
using System.Collections.Generic;
34

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

11+
public class Resource<T>
12+
{
13+
public int count { get; set; }
14+
public IEnumerable<T> value { get; set; }
15+
}
16+
17+
public class Account
18+
{
19+
public string accountId { get; set; }
20+
public string accountName { get; set; }
21+
public string accountOwner { get; set; }
22+
public Dictionary<string, object> properties { get; set; }
23+
public string Mail
24+
{
25+
get
26+
{
27+
if (properties.ContainsKey("Mail"))
28+
{
29+
switch (properties["Mail"])
30+
{
31+
case JObject mailObject:
32+
return mailObject.Property("$value").Value.ToString();
33+
34+
case JProperty mailProp:
35+
return mailProp.Value.ToString();
36+
37+
case string mailString:
38+
return mailString;
39+
}
40+
}
41+
42+
return string.Empty;
43+
}
44+
}
45+
}
46+
1047
public class Avatar
1148
{
1249
public string href { get; set; }
@@ -75,13 +112,23 @@ public class PullRequest
75112
public string Url { get; set; }
76113
public bool SupportsIterations { get; set; }
77114
public Creator CreatedBy { get; set; }
115+
public IEnumerable<WebApiTagDefinition> labels { get; set; }
78116

79117
// public CreatedBy CreatedBy { get; set; }
80118
// public Lastmergesourcecommit LastMergeSourceCommit { get; set; }
81119
// public Lastmergetargetcommit LastMergeTargetCommit { get; set; }
82120
// public Lastmergecommit LastMergeCommit { get; set; }
83121
// public IEnumerable<Reviewer> Reviewers { get; set; }
84122
}
123+
124+
public class WebApiTagDefinition
125+
{
126+
public bool active { get; set; }
127+
public string id { get; set; }
128+
public string name { get; set; }
129+
public string url { get; set; }
130+
}
131+
85132
public class ProjectResource
86133
{
87134
public int Count { get; set; }

NuKeeper.BitBucket/BitbucketPlatform.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,5 +139,10 @@ private static Repository MapRepository(BitBucket.Models.Repository repo)
139139
new Uri(repo.links.html.href),
140140
null, false, null);
141141
}
142+
143+
public Task<int> GetNumberOfOpenPullRequests(string projectName, string repositoryName)
144+
{
145+
return Task.FromResult(0);
146+
}
142147
}
143148
}

NuKeeper.GitHub/OctokitClient.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,5 +263,10 @@ private static async Task<T> ExceptionHandler<T>(Func<Task<T>> funcToCheck)
263263
throw new NuKeeperException(ex.Message, ex);
264264
}
265265
}
266+
267+
public Task<int> GetNumberOfOpenPullRequests(string projectName, string repositoryName)
268+
{
269+
return Task.FromResult(0);
270+
}
266271
}
267272
}

0 commit comments

Comments
 (0)