515 строки
21 KiB
C#
515 строки
21 KiB
C#
using dotnetthanks;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Octokit;
|
|
using System.Diagnostics;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace dotnetthanks_loader
|
|
{
|
|
class Program
|
|
{
|
|
private static HttpClient _client;
|
|
private static readonly string[] exclusions = new string[]
|
|
{
|
|
"dependabot[bot]",
|
|
"github-actions[bot]",
|
|
"msftbot[bot]",
|
|
"github-actions[bot]",
|
|
"dotnet bot",
|
|
"dotnet-bot",
|
|
"nuget team bot",
|
|
"net source-build bot",
|
|
"dotnet-maestro-bot",
|
|
"dotnet-maestro[bot]"
|
|
};
|
|
private static string _token;
|
|
|
|
private static GitHubClient _ghclient;
|
|
|
|
static async Task Main(string[] args)
|
|
{
|
|
var config = new ConfigurationBuilder()
|
|
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
|
|
.AddEnvironmentVariables()
|
|
.AddUserSecrets<Program>()
|
|
.Build();
|
|
|
|
_token = config.GetSection("GITHUB_TOKEN").Value;
|
|
|
|
_ghclient = new GitHubClient(new ProductHeaderValue("dotnet-thanks"));
|
|
var basic = new Credentials(config.GetSection("GITHUB_CLIENTID").Value, config.GetSection("GITHUB_CLIENTSECRET").Value);
|
|
_ghclient.Credentials = basic;
|
|
|
|
var repo = "core";
|
|
var owner = "dotnet";
|
|
|
|
// load all releases for dotnet/core
|
|
IEnumerable<dotnetthanks.Release> allReleases = await LoadReleasesAsync(owner, repo);
|
|
|
|
// Sort releases from the yongest to the oldest by version
|
|
// E.g.
|
|
// 5.0.1
|
|
// 5.0.0 // GA
|
|
// 5.0.0-RC2
|
|
// 5.0.0-RC1
|
|
// 5.0.0-preview9
|
|
// ...
|
|
// 5.0.0-preview2
|
|
// 3.1.10
|
|
// 3.1.9
|
|
// 3.1.8
|
|
// ...
|
|
// 3.1.0 // GA
|
|
// ...
|
|
//
|
|
List<dotnetthanks.Release> sortedReleases = allReleases
|
|
.OrderByDescending(o => o.Version)
|
|
.ThenByDescending(o => o.Id)
|
|
.ToList();
|
|
|
|
Dictionary<string, dotnetthanks.MajorRelease> sortedMajorReleasesDictionary = new ();
|
|
List<dotnetthanks.MajorRelease> sortedMajorReleasesList = new ();
|
|
|
|
// If arg 1 is "diff" calculate the diff and append it to current core.js file
|
|
if (args != null && args.Length > 0 && args[0] == "diff")
|
|
{
|
|
// load current core.json file
|
|
#if DEBUG
|
|
IEnumerable<dotnetthanks.MajorRelease> corejson = LoadCurrentCoreJson();
|
|
#else
|
|
IEnumerable<dotnetthanks.MajorRelease> corejson = await LoadCurrentCoreJsonAsync();
|
|
#endif
|
|
// create a dictionary with preprocessed data
|
|
foreach (var release in corejson)
|
|
{
|
|
sortedMajorReleasesDictionary.Add($"{release.Version.Major}.{release.Version.Minor}", release);
|
|
}
|
|
|
|
List<string> processedReleases = new();
|
|
|
|
foreach (var o in corejson)
|
|
{
|
|
processedReleases.AddRange(o.ProcessedReleases);
|
|
}
|
|
|
|
List<dotnetthanks.Release> diff = sortedReleases.Where(o => !processedReleases.Contains(o.Tag)).ToList();
|
|
|
|
// Check if releases in diff are not in dictionary
|
|
foreach (var release in diff)
|
|
{
|
|
if (!sortedMajorReleasesDictionary.ContainsKey($"{release.Version.Major}.{release.Version.Minor}"))
|
|
{
|
|
sortedMajorReleasesDictionary.Add($"{release.Version.Major}.{release.Version.Minor}", new MajorRelease
|
|
{
|
|
Contributions = 0,
|
|
Contributors = new(),
|
|
Product = release.Product,
|
|
Name = $"{release.Product} {release.Version.Major}.{release.Version.Minor}",
|
|
Version = release.Version,
|
|
Tag = $"v{release.Version.Major}.{release.Version.Minor}",
|
|
ProcessedReleases = new()
|
|
});
|
|
}
|
|
}
|
|
|
|
if (diff.Count != 0)
|
|
{
|
|
Console.WriteLine($"Processing diffs in releases...\n{repo} - {diff.Count}");
|
|
|
|
// For each new release, find its prior release and add it into a new list for commit comparison
|
|
List<dotnetthanks.Release> sortedNewReleases = new ();
|
|
List<dotnetthanks.Release> majorReleasesList = new ();
|
|
var latestGARelease = sortedReleases.ToList().Find(r => r.IsGA);
|
|
|
|
foreach(var r in diff)
|
|
{
|
|
var idx = sortedReleases.IndexOf(r);
|
|
var previousVersion = sortedReleases[idx + 1];
|
|
|
|
// Add new release
|
|
sortedNewReleases.Add(r);
|
|
|
|
// If previous version isn't in newReleases already - add it
|
|
if (!diff.Contains(previousVersion))
|
|
{
|
|
// Append the latest processed release or last GA if first release
|
|
if (r.Version.Major != previousVersion.Version.Major)
|
|
{
|
|
sortedNewReleases.Add(latestGARelease);
|
|
}
|
|
else if (r.Version.Major == previousVersion.Version.Major)
|
|
{
|
|
sortedNewReleases.Add(previousVersion);
|
|
}
|
|
}
|
|
else if (diff.Contains(previousVersion) && r.Version.Major != previousVersion.Version.Major)
|
|
{
|
|
sortedNewReleases.Add(latestGARelease);
|
|
}
|
|
}
|
|
|
|
sortedNewReleases = sortedNewReleases
|
|
.OrderByDescending(o => o.Version)
|
|
.ThenByDescending(o => o.Id)
|
|
.ToList();
|
|
|
|
// Process new list and trim the releases used for comparison
|
|
await ProcessReleases(sortedNewReleases, sortedMajorReleasesDictionary, repo, true);
|
|
|
|
sortedMajorReleasesList = sortedMajorReleasesDictionary.Values.OrderByDescending(o => o.Version).ToList();
|
|
|
|
File.WriteAllText($"./{repo}.json", JsonSerializer.Serialize(sortedMajorReleasesList));
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("The current releases list is up to date with core.js\nExiting...");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// create a dictionary for major versions
|
|
foreach (var release in sortedReleases)
|
|
{
|
|
if (!sortedMajorReleasesDictionary.ContainsKey($"{release.Version.Major}.{release.Version.Minor}"))
|
|
{
|
|
var majorRelease = new MajorRelease
|
|
{
|
|
Contributions = 0,
|
|
Contributors = new (),
|
|
Product = release.Product,
|
|
Name = $"{release.Product} {release.Version.Major}.{release.Version.Minor}",
|
|
Version = release.Version,
|
|
Tag = $"v{release.Version.Major}.{release.Version.Minor}",
|
|
ProcessedReleases = new ()
|
|
};
|
|
sortedMajorReleasesDictionary.Add($"{release.Version.Major}.{release.Version.Minor}", majorRelease);
|
|
}
|
|
}
|
|
|
|
Console.WriteLine($"Processing all releases...\n{repo} - {sortedReleases.Count}");
|
|
|
|
await ProcessReleases(sortedReleases, sortedMajorReleasesDictionary, repo);
|
|
|
|
sortedMajorReleasesList = sortedMajorReleasesDictionary.Values.ToList();
|
|
|
|
File.WriteAllText($"./{repo}.json", JsonSerializer.Serialize(sortedMajorReleasesList));
|
|
}
|
|
}
|
|
|
|
#nullable enable
|
|
private static async Task ProcessReleases(List<dotnetthanks.Release> releases, Dictionary<string, dotnetthanks.MajorRelease> majorReleasesDict, string repo, bool isDiff = false)
|
|
{
|
|
// dotnet/core
|
|
dotnetthanks.Release currentRelease;
|
|
dotnetthanks.Release previousRelease;
|
|
for (int i = 0; i < releases.Count; i++)
|
|
{
|
|
currentRelease = releases[i];
|
|
majorReleasesDict.TryGetValue($"{currentRelease.Version.Major}.{currentRelease.Version.Minor}", out var majorRelease);
|
|
|
|
// Add the first release to the list of processed releases so diff does not pick it up
|
|
if (i == releases.Count - 1)
|
|
{
|
|
if (!isDiff)
|
|
{
|
|
majorRelease?.ProcessedReleases.Add(currentRelease.Tag);
|
|
}
|
|
break;
|
|
}
|
|
|
|
previousRelease = GetPreviousRelease(releases, currentRelease, i + 1);
|
|
|
|
if (previousRelease is null)
|
|
{
|
|
// Is this the first release?
|
|
Console.WriteLine($"[INFO]: {currentRelease.Tag} is the first release in the series.");
|
|
//Debugger.Break();
|
|
continue;
|
|
}
|
|
|
|
majorRelease?.ProcessedReleases.Add(currentRelease.Tag);
|
|
|
|
Console.WriteLine($"Processing:[{i}] {repo} {previousRelease.Tag}..{currentRelease.Tag}");
|
|
|
|
// for each child repo get commits and count contribs
|
|
foreach (var repoCurrentRelease in currentRelease.ChildRepos)
|
|
{
|
|
var repoPrevRelease = previousRelease.ChildRepos.FirstOrDefault(r => r.Owner == repoCurrentRelease.Owner &&
|
|
r.Repository == repoCurrentRelease.Repository);
|
|
if (repoPrevRelease is null)
|
|
{
|
|
// This may happen
|
|
Console.WriteLine($"[ERROR]: {repoCurrentRelease.Url} doesn't exist in {previousRelease.Tag}!");
|
|
continue;
|
|
}
|
|
|
|
Debug.WriteLine($"{repoCurrentRelease.Tag} : {repoCurrentRelease.Name}");
|
|
|
|
try
|
|
{
|
|
Console.WriteLine($"\tProcessing: {repoCurrentRelease.Name}: {repoPrevRelease.Tag}..{repoCurrentRelease.Tag}");
|
|
|
|
if (repoPrevRelease.Tag != repoCurrentRelease.Tag)
|
|
{
|
|
var releaseDiff = await LoadCommitsForReleasesAsync(repoPrevRelease.Tag,
|
|
repoCurrentRelease.Tag,
|
|
repoCurrentRelease.Owner,
|
|
repoCurrentRelease.Repository);
|
|
if (releaseDiff is null || releaseDiff.Count < 1)
|
|
{
|
|
//Debugger.Break();
|
|
continue;
|
|
}
|
|
|
|
if (majorRelease is not null)
|
|
{
|
|
majorRelease.Contributions += releaseDiff.Count;
|
|
TallyCommits(majorRelease, repoCurrentRelease.Repository, releaseDiff);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
}
|
|
|
|
if (Environment.GetEnvironmentVariable("TEST") == "1")
|
|
break;
|
|
}
|
|
}
|
|
#nullable disable
|
|
|
|
private static async Task<PullRequest> CreatePullRequestFromFork(string forkname, string branch)
|
|
{
|
|
var basic = new Credentials(_token);
|
|
var client = new GitHubClient(new ProductHeaderValue("dotnet-thanks"))
|
|
{
|
|
Credentials = basic
|
|
};
|
|
|
|
NewPullRequest newPr = new("Update thanks data file", $"spboyer:{branch}", "master");
|
|
|
|
try
|
|
{
|
|
var pullRequest = await client.PullRequest.Create("dotnet", "website-resources", newPr);
|
|
|
|
return pullRequest;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find the previous release for the current release in the sorted collection of all releases.
|
|
/// Take the immediate previous release it it has the same major.minor version (e.g. 5.0.0-RC1 for 5.0.0-RC2),
|
|
/// or take the previous GA release (e.g. 3.0.0 for 5.0.0-preview2 not 3.1.10).
|
|
/// </summary>
|
|
/// <param name="index">The index of the <paramref name="currentRelease"/> in the <paramref name="sortedReleases"/> list.</param>
|
|
/// <returns>The previous release, if found; otherwise <see cref="null"/>, if the current release if the first release.</returns>
|
|
private static dotnetthanks.Release GetPreviousRelease(List<dotnetthanks.Release> sortedReleases, dotnetthanks.Release currentRelease, int index)
|
|
{
|
|
if (currentRelease.Version.Major == sortedReleases[index].Version.Major &&
|
|
currentRelease.Version.Minor == sortedReleases[index].Version.Minor)
|
|
return sortedReleases[index];
|
|
|
|
return sortedReleases.Skip(index).FirstOrDefault(r => currentRelease.Version > r.Version && r.IsGA);
|
|
}
|
|
|
|
private static void TallyCommits(dotnetthanks.MajorRelease majorRelease, string repoName, List<MergeBaseCommit> commits)
|
|
{
|
|
// these the commits within the release
|
|
foreach (var item in commits)
|
|
{
|
|
if (item.author != null)
|
|
{
|
|
var author = item.author;
|
|
author.name = item.commit.author.name;
|
|
|
|
if (string.IsNullOrEmpty(author.name))
|
|
author.name = "Unknown";
|
|
|
|
if (!exclusions.Contains(author.name.ToLower()))
|
|
{
|
|
// find if the author has been counted
|
|
var person = majorRelease.Contributors.Find(p => p.Link == author.html_url);
|
|
if (person == null)
|
|
{
|
|
person = new dotnetthanks.Contributor()
|
|
{
|
|
Name = author.name,
|
|
Link = author.html_url,
|
|
Avatar = author.avatar_url,
|
|
Count = 1
|
|
};
|
|
person.Repos.Add(new RepoItem() { Name = repoName, Count = 1 });
|
|
|
|
majorRelease.Contributors.Add(person);
|
|
}
|
|
else
|
|
{
|
|
// found the author, does the repo exist as well?
|
|
person.Count += 1;
|
|
|
|
var repoItem = person.Repos.Find(r => r.Name == repoName);
|
|
if (repoItem == null)
|
|
{
|
|
person.Repos.Add(new RepoItem() { Name = repoName, Count = 1 });
|
|
}
|
|
else
|
|
{
|
|
repoItem.Count += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static async Task<IEnumerable<dotnetthanks.Release>> LoadReleasesAsync(string owner, string repo)
|
|
{
|
|
var results = await _ghclient.Repository.Release.GetAll(owner, repo);
|
|
|
|
return results.Select(release => new dotnetthanks.Release
|
|
{
|
|
Name = release.Name,
|
|
Tag = release.TagName,
|
|
Id = release.Id,
|
|
ChildRepos = ParseReleaseBody(release.Body),
|
|
Contributors = new List<dotnetthanks.Contributor>()
|
|
});
|
|
}
|
|
|
|
private static List<ChildRepo> ParseReleaseBody(string body)
|
|
{
|
|
var results = new List<ChildRepo>();
|
|
|
|
var pattern = "\\[(.+)\\]\\(([^ ]+?)( \"(.+)\")?\\)";
|
|
var rg = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline);
|
|
var match = rg.Match(body);
|
|
|
|
while (match.Success)
|
|
{
|
|
var name = match.Groups[1]?.Value.Trim();
|
|
var url = match.Groups[2]?.Value.Trim();
|
|
if (url.Contains("/tag/"))
|
|
{
|
|
results.Add(new ChildRepo() { Name = name, Url = url });
|
|
}
|
|
|
|
match = match.NextMatch();
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
private static async Task<List<MergeBaseCommit>> LoadCommitsForReleasesAsync(string fromRelease, string toRelease, string owner, string repo)
|
|
{
|
|
_client = new HttpClient();
|
|
_client.DefaultRequestHeaders.UserAgent.Add(new System.Net.Http.Headers.ProductInfoHeaderValue("dotnet-thanks", "1.0"));
|
|
_client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Token", _token);
|
|
|
|
try
|
|
{
|
|
string url = $"https://api.github.com/repos/{owner}/{repo}/compare/{fromRelease}...{toRelease}";
|
|
var compare = await ExecuteWithRateLimitHandling(() => _client.GetStringAsync(url));
|
|
|
|
var compareDetails = JsonSerializer.Deserialize<Root>(compare);
|
|
|
|
var remainingCommits = compareDetails.ahead_by;
|
|
var page = 0;
|
|
var releaseCommits = new List<MergeBaseCommit>(remainingCommits);
|
|
while (remainingCommits > 0)
|
|
{
|
|
url = $"https://api.github.com/repos/{owner}/{repo}/commits?sha={toRelease}&page={page}";
|
|
var commits = await ExecuteWithRateLimitHandling(() => _client.GetStringAsync(url));
|
|
var pageDetails = JsonSerializer.Deserialize<List<MergeBaseCommit>>(commits);
|
|
releaseCommits.AddRange(remainingCommits >= pageDetails.Count ? pageDetails : pageDetails.Take(remainingCommits));
|
|
remainingCommits -= pageDetails.Count;
|
|
page++;
|
|
}
|
|
|
|
return releaseCommits;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static List<dotnetthanks.MajorRelease> LoadCurrentCoreJson()
|
|
{
|
|
try
|
|
{
|
|
string fileName = "core.json";
|
|
string jsonString = File.ReadAllText(fileName);
|
|
var corejson = JsonSerializer.Deserialize<List<dotnetthanks.MajorRelease>>(jsonString);
|
|
return corejson;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static async Task<List<dotnetthanks.MajorRelease>> LoadCurrentCoreJsonAsync()
|
|
{
|
|
_client = new HttpClient();
|
|
var url = "https://dotnetwebsitestorage.blob.core.windows.net/blob-assets/json/thanks/core.json";
|
|
|
|
try
|
|
{
|
|
var response = await _client.GetFromJsonAsync<List<dotnetthanks.MajorRelease>>(url);
|
|
return response;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static async Task<T> ExecuteWithRateLimitHandling<T>(Func<Task<T>> operation)
|
|
{
|
|
var remainingRetries = 3;
|
|
Retry:
|
|
try
|
|
{
|
|
return await operation();
|
|
}
|
|
catch (Exception ex) when (remainingRetries > 0)
|
|
{
|
|
if (ex.Message.Contains("403"))
|
|
{
|
|
string url = "https://api.github.com/rate_limit";
|
|
var limit = await _client.GetStringAsync(url);
|
|
var response = JsonSerializer.Deserialize<RateLimit>(limit);
|
|
var delay = new TimeSpan(response.rate.reset)
|
|
.Add(TimeSpan.FromMinutes(10)); // Add some buffer
|
|
var until = DateTime.Now.Add(delay);
|
|
Console.WriteLine($"Rate limit exceeded. Waiting for {delay.TotalMinutes:N1} mins until {until}.");
|
|
await Task.Delay(delay);
|
|
remainingRetries--;
|
|
goto Retry;
|
|
}
|
|
else
|
|
{
|
|
return await Task.FromException<T>(ex);
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|