Created
March 27, 2019 16:00
-
-
Save MattJeanes/656567daf6c03fd405506a87df8f7671 to your computer and use it in GitHub Desktop.
Release Notes Helper for Azure DevOps
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Microsoft.Extensions.Logging; | |
using Microsoft.Extensions.Options; | |
using Microsoft.TeamFoundation.SourceControl.WebApi; | |
using Microsoft.TeamFoundation.WorkItemTracking.WebApi; | |
using Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models; | |
using Microsoft.VisualStudio.Services.WebApi; | |
using Microsoft.VisualStudio.Services.WebApi.Patch; | |
using Microsoft.VisualStudio.Services.WebApi.Patch.Json; | |
using Newtonsoft.Json; | |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
using VSTSIntegration.Model.Data; | |
namespace VSTSIntegration.Model.Helpers | |
{ | |
public interface IReleaseNotesHelper | |
{ | |
Task<ReleaseNotes> GetReleaseNotes(int pullRequestId, bool includeMerges = false, bool includeSprintBugs = false, bool updateStatusOnly = false); | |
} | |
public class ReleaseNotesHelper : IReleaseNotesHelper | |
{ | |
const string mergeCacheKey = "Moneybarn.MergeCache"; | |
private readonly Dictionary<string, bool> _mergeCache = new Dictionary<string, bool>(); | |
private readonly AppSettings _appSettings; | |
private readonly ILogger _logger; | |
private readonly VssConnection _vssConnection; | |
private GitHttpClient __gitClient; | |
private async Task<GitHttpClient> GetGitClient() | |
{ | |
return __gitClient ?? (__gitClient = await _vssConnection.GetClientAsync<GitHttpClient>()); | |
} | |
private WorkItemTrackingHttpClient __workItemClient; | |
private async Task<WorkItemTrackingHttpClient> GetWorkItemClient() | |
{ | |
return __workItemClient ?? (__workItemClient = await _vssConnection.GetClientAsync<WorkItemTrackingHttpClient>()); | |
} | |
public ReleaseNotesHelper(VssConnection vssConnection, ILogger<ReleaseNotesHelper> logger, IOptions<AppSettings> appSettings) | |
{ | |
_vssConnection = vssConnection; | |
_logger = logger; | |
_appSettings = appSettings.Value; | |
} | |
public async Task<ReleaseNotes> GetReleaseNotes(int pullRequestId, bool includeMerges = false, bool includeSprintBugs = false, bool updateStatusOnly = false) | |
{ | |
var releaseNotes = new ReleaseNotes(); | |
var gitClient = await GetGitClient(); | |
var pullRequest = await gitClient.GetPullRequestByIdAsync(pullRequestId); | |
releaseNotes.PullRequest = pullRequest; | |
var iterations = await gitClient.GetPullRequestIterationsAsync(pullRequest.Repository.Id, pullRequestId); | |
var iteration = iterations.OrderByDescending(x => x.Id).FirstOrDefault(); | |
var mergePolicyApplicable = IsMergePolicyApplicable(pullRequest); | |
if (pullRequest.Status == PullRequestStatus.Active) | |
{ | |
if (mergePolicyApplicable) | |
{ | |
await UpdateStatus(pullRequest, iteration, new MergePolicy | |
{ | |
Description = "Pending", | |
State = GitStatusState.Pending | |
}); | |
} | |
else | |
{ | |
await UpdateStatus(pullRequest, iteration, new MergePolicy | |
{ | |
Description = "N/A", | |
State = GitStatusState.NotApplicable | |
}); | |
} | |
} | |
if (!updateStatusOnly || (pullRequest.Status == PullRequestStatus.Active && mergePolicyApplicable)) | |
{ | |
try | |
{ | |
releaseNotes.Commits = await GetCommits(pullRequest, iteration, includeMerges); | |
releaseNotes.WorkItems = await GetWorkItems(releaseNotes.Commits, includeSprintBugs); | |
if (pullRequest.Status == PullRequestStatus.Active && mergePolicyApplicable) | |
{ | |
releaseNotes.MergePolicy = GetMergePolicy(releaseNotes); | |
await UpdateStatus(pullRequest, iteration, releaseNotes.MergePolicy); | |
} | |
} | |
catch (Exception e) | |
{ | |
if (pullRequest.Status == PullRequestStatus.Active) | |
{ | |
try | |
{ | |
await UpdateStatus(pullRequest, iteration, new MergePolicy | |
{ | |
State = GitStatusState.Error, | |
Description = e.Message | |
}); | |
} | |
catch (Exception e2) | |
{ | |
_logger.LogError("Failed to log Error to pull request"); | |
_logger.LogError(e2.ToString()); | |
} | |
} | |
throw; | |
}; | |
} | |
return releaseNotes; | |
} | |
private bool IsMergePolicyApplicable(GitPullRequest pullRequest) | |
{ | |
return pullRequest.TargetRefName.StartsWith("refs/heads/master"); | |
} | |
private MergePolicy GetMergePolicy(ReleaseNotes releaseNotes) | |
{ | |
var states = new string[] { "Done", "Removed" }; | |
if (releaseNotes?.WorkItems?.Any(x => !states.Contains(x.Fields["System.State"])) ?? false) | |
{ | |
return new MergePolicy | |
{ | |
Description = "Work items incomplete", | |
State = GitStatusState.Failed | |
}; | |
} | |
else | |
{ | |
return new MergePolicy | |
{ | |
Description = "Work items completed", | |
State = GitStatusState.Succeeded | |
}; | |
} | |
} | |
private async Task UpdateStatus(GitPullRequest pullRequest, GitPullRequestIteration iteration, MergePolicy mergePolicy) | |
{ | |
var gitClient = await GetGitClient(); | |
var status = new GitPullRequestStatus | |
{ | |
Context = new GitStatusContext | |
{ | |
Genre = "moneybarn", | |
Name = "workitems", | |
}, | |
TargetUrl = $"{_appSettings.Url}/releasenotes/{pullRequest.PullRequestId}", | |
IterationId = iteration.Id, | |
State = mergePolicy.State, | |
Description = mergePolicy.Description | |
}; | |
_logger.LogInformation($"Marking PR {pullRequest.PullRequestId} as {status.State.ToString()}"); | |
await gitClient.CreatePullRequestStatusAsync(status, pullRequest.Repository.Id, pullRequest.PullRequestId); | |
} | |
private async Task<IOrderedEnumerable<GitCommitRef>> GetCommits(GitPullRequest pullRequest, GitPullRequestIteration iteration, bool includeMerges) | |
{ | |
var gitClient = await GetGitClient(); | |
var baseVersion = new GitVersionDescriptor | |
{ | |
Version = iteration.TargetRefCommit?.CommitId ?? throw new Exception("Merge incomplete"), | |
VersionType = GitVersionType.Commit | |
}; | |
var targetVersion = new GitVersionDescriptor | |
{ | |
Version = iteration.SourceRefCommit?.CommitId ?? throw new Exception("Merge incomplete"), | |
VersionType = GitVersionType.Commit | |
}; | |
var searchCriteria = new GitQueryCommitsCriteria | |
{ | |
ItemVersion = baseVersion, | |
CompareVersion = targetVersion, | |
IncludeWorkItems = true | |
}; | |
var top = 100; | |
var skip = 0; | |
var commits = new List<GitCommitRef>(); | |
var properties = await gitClient.GetPullRequestPropertiesAsync(pullRequest.Repository.Id, pullRequest.PullRequestId); | |
var mergeCache = properties.ContainsKey(mergeCacheKey) | |
? JsonConvert.DeserializeObject<Dictionary<string, bool>>(properties[mergeCacheKey].ToString()) | |
: new Dictionary<string, bool>(); | |
mergeCache.ToList().ForEach(x => _mergeCache[x.Key] = x.Value); | |
var mergeCacheModified = false; | |
while (true) | |
{ | |
var batch = await gitClient.GetCommitsBatchAsync(searchCriteria, pullRequest.Repository.Id, skip, top); | |
if (batch.Count == 0) { break; } | |
if (!includeMerges) | |
{ | |
foreach (var commitRef in batch) | |
{ | |
if (!mergeCache.ContainsKey(commitRef.CommitId)) | |
{ | |
if (_mergeCache.ContainsKey(commitRef.CommitId)) | |
{ | |
mergeCache[commitRef.CommitId] = _mergeCache[commitRef.CommitId]; | |
} | |
else | |
{ | |
// Unfortunately GetCommitsBatch does not return parent info so we must query one-by-one :( | |
var commit = await gitClient.GetCommitAsync(commitRef.CommitId, pullRequest.Repository.Id); | |
mergeCache[commitRef.CommitId] = commit.Parents.Count() > 1; | |
_mergeCache[commitRef.CommitId] = mergeCache[commitRef.CommitId]; | |
} | |
mergeCacheModified = true; | |
} | |
} | |
batch.RemoveAll(x => _mergeCache[x.CommitId]); | |
} | |
commits.AddRange(batch); | |
skip += top; | |
} | |
if (mergeCacheModified && pullRequest.Status == PullRequestStatus.Active) | |
{ | |
var patch = new JsonPatchDocument | |
{ | |
new JsonPatchOperation | |
{ | |
Operation = Operation.Replace, | |
Path = $"/{mergeCacheKey}", | |
Value = JsonConvert.SerializeObject(mergeCache) | |
} | |
}; | |
await gitClient.UpdatePullRequestPropertiesAsync(patch, pullRequest.Repository.Id, pullRequest.PullRequestId); | |
} | |
return commits.OrderByDescending(x => x.Author.Date); | |
} | |
private async Task<IOrderedEnumerable<WorkItem>> GetWorkItems(IEnumerable<GitCommitRef> commits, bool includeSprintBugs) | |
{ | |
var workItemClient = await GetWorkItemClient(); | |
var workItemRefs = commits.SelectMany(x => x.WorkItems).ToList(); | |
var workItemIds = workItemRefs.Select(x => int.Parse(x.Id)).Distinct().ToList(); | |
if (workItemIds.Count > 0) | |
{ | |
var workItems = await workItemClient.GetWorkItemsAsync(workItemIds, expand: WorkItemExpand.All); | |
if (!includeSprintBugs) | |
{ | |
workItems.RemoveAll(x => x.Fields["System.WorkItemType"].ToString() == "Bug" | |
&& x.Fields.ContainsKey("MBScrum.BugSource") | |
&& !new string[] { "", "Unknown", "Production" }.Contains(x.Fields["MBScrum.BugSource"].ToString())); | |
} | |
return workItems.OrderByDescending(x => x.Id); | |
} | |
return null; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment