Skip to content

Commit

Permalink
Merge pull request #20 from lgrabarevic/feature/group-by-pullrequest
Browse files Browse the repository at this point in the history
Feature/group by pullrequest
  • Loading branch information
Luka Grabarevic authored May 17, 2018
2 parents 9278de4 + 710c309 commit b28ecd8
Show file tree
Hide file tree
Showing 36 changed files with 708 additions and 53 deletions.
50 changes: 36 additions & 14 deletions BuildsAppReborn.Access/BuildMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,11 @@
using System.Linq;
using System.Threading.Tasks;
using System.Timers;

using BuildsAppReborn.Contracts;
using BuildsAppReborn.Contracts.Composition;
using BuildsAppReborn.Contracts.Models;
using BuildsAppReborn.Contracts.UI.Notifications;
using BuildsAppReborn.Infrastructure;

using log4net;

namespace BuildsAppReborn.Access
Expand All @@ -23,9 +21,10 @@ internal sealed class BuildMonitor : IBuildMonitorAdvanced
#region Constructors

[ImportingConstructor]
public BuildMonitor(LazyContainer<IBuildProvider, IBuildProviderMetadata> buildProviders, LazyContainer<INotificationProvider, IPriorityMetadata> notificationProviders)
public BuildMonitor(LazyContainer<IBuildProvider, IBuildProviderMetadata> buildProviders, LazyContainer<INotificationProvider, IPriorityMetadata> notificationProviders, IEqualityComparer<IBuildDefinition> buildDefinitionEqualityComparer)
{
this.buildProviders = buildProviders;
this.buildDefinitionEqualityComparer = buildDefinitionEqualityComparer;
this.notificationProvider = notificationProviders.GetSupportedNotificationProvider();
#pragma warning disable 4014
this.timer.Elapsed += (sender, args) => BeginPollingBuilds();
Expand All @@ -36,15 +35,17 @@ public BuildMonitor(LazyContainer<IBuildProvider, IBuildProviderMetadata> buildP

#region Implementation of IBuildMonitorAdvanced

public void Start(IEnumerable<BuildMonitorSettings> settings, TimeSpan pollingInterval)
public void Start(IEnumerable<BuildMonitorSettings> settings, GeneralSettings generalSettingsParam)
{
Stop();

Initialize(settings);

this.generalSettings = generalSettingsParam;

if (this.providerSettingsGroup.Any())
{
this.timer.Interval = pollingInterval.TotalMilliseconds;
this.timer.Interval = this.generalSettings.PollingInterval.TotalMilliseconds;
this.timer.Start();
}

Expand All @@ -55,6 +56,7 @@ public void Stop()
{
this.timer.Stop();
this.providerSettingsGroup.Clear();
this.generalSettings = null;

OnMonitorStopped();
}
Expand Down Expand Up @@ -122,17 +124,34 @@ private async Task<IEnumerable<IBuild>> PollBuilds(IBuildProvider provider, Buil
{
try
{
var builds = await Task.Run(() => provider.GetBuilds(settings.SelectedBuildDefinitions, settings));

if (!builds.IsSuccessStatusCode)
var builds = new DataResponse<IEnumerable<IBuild>>();
if (this.generalSettings?.ViewStyle == BuildViewStyle.GroupByPullRequest)
{
this.logger.Warn($"Http status code {builds.StatusCode} returned while polling for builds!");
this.notificationProvider?.ShowMessage("Failure on getting builds", $"Please check the connection for project(s) '{String.Join(", ", settings.SelectedBuildDefinitions.Select(b => b.Project.Name).Distinct())}'. StatusCode was '{builds.StatusCode}'. See log for more details.");
}
else
{
return builds.Data;
var prBuilds = await provider.GetBuildsByPullRequests(settings);
prBuilds.ThrowIfUnsuccessful();

var definitionsInUse = prBuilds.Data.GroupBy(a => a.Definition, build => build, this.buildDefinitionEqualityComparer).Select(a => a.Key);
var unusedDefinitions = settings.SelectedBuildDefinitions.Except(definitionsInUse, this.buildDefinitionEqualityComparer).ToList();
if (unusedDefinitions.Any())
{
var defBuilds = await provider.GetBuilds(settings.SelectedBuildDefinitions, settings);
defBuilds.ThrowIfUnsuccessful();

return prBuilds.Data.Concat(defBuilds.Data);
}

return prBuilds.Data;
}

builds = await provider.GetBuilds(settings.SelectedBuildDefinitions, settings);
builds.ThrowIfUnsuccessful();

return builds.Data;
}
catch (DataResponseUnsuccessfulException ex)
{
this.logger.Warn($"Http status code {ex.StatusCode} returned while polling for builds!");
this.notificationProvider?.ShowMessage("Failure on getting builds", $"Please check the connection for project(s) '{String.Join(", ", settings.SelectedBuildDefinitions.Select(b => b.Project.Name).Distinct())}'. StatusCode was '{ex.StatusCode}'. See log for more details.");
}
catch (Exception exception)
{
Expand All @@ -147,7 +166,10 @@ private async Task<IEnumerable<IBuild>> PollBuilds(IBuildProvider provider, Buil

#region Private Fields

private readonly IEqualityComparer<IBuildDefinition> buildDefinitionEqualityComparer;

private readonly LazyContainer<IBuildProvider, IBuildProviderMetadata> buildProviders;
private GeneralSettings generalSettings;

private Boolean isPolling;

Expand Down
3 changes: 3 additions & 0 deletions BuildsAppReborn.Access/BuildsAppReborn.Access.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
<Compile Include="TFS2017\Models\Tfs2017Build.cs" />
<Compile Include="TFS2017\Models\Tfs2017BuildDefinition.cs" />
<Compile Include="TFS2017\Models\Tfs2017Project.cs" />
<Compile Include="TFS2017\Models\Tfs2017PullRequest.cs" />
<Compile Include="TFS2017\Models\Tfs2017SourceVersion.cs" />
<Compile Include="TFS2017\Models\Tfs2017User.cs" />
<Compile Include="TFS2017\Models\TfsTestRun.cs" />
Expand All @@ -75,12 +76,14 @@
<Compile Include="TFS\Models\TfsBuild.cs" />
<Compile Include="TFS\Models\TfsBuildDefinition.cs" />
<Compile Include="TFS\Models\TfsProject.cs" />
<Compile Include="TFS\Models\TfsPullRequest.cs" />
<Compile Include="TFS\Models\TfsSourceVersion.cs" />
<Compile Include="TFS\Models\TfsTestRun.cs" />
<Compile Include="TFS\Models\TfsUser.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TFS\Models\TfsArtifact.cs" />
<Compile Include="TFS\TfsBuildProviderBase.cs" />
<Compile Include="VSTS\Models\VstsPullRequest.cs" />
<Compile Include="VSTS\Models\VstsTestRun.cs" />
<Compile Include="VSTS\Models\VstsArtifact.cs" />
<Compile Include="VSTS\Models\VstsBuild.cs" />
Expand Down
3 changes: 3 additions & 0 deletions BuildsAppReborn.Access/TFS/Models/TfsBuild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ public TfsBuild()
[JsonProperty("id")]
public Int32 Id { get; private set; }

[JsonIgnore]
public IPullRequest PullRequest { get; internal set; }

[JsonIgnore]
public BuildStatus Status
{
Expand Down
37 changes: 37 additions & 0 deletions BuildsAppReborn.Access/TFS/Models/TfsPullRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using BuildsAppReborn.Contracts.Models;
using Newtonsoft.Json;

namespace BuildsAppReborn.Access.Models
{
internal abstract class TfsPullRequest : IPullRequest
{
#region Implementation of IPullRequest

[JsonProperty("createdBy")]
public virtual IUser CreatedBy { get; protected set; }

[JsonProperty("description")]
public String Description { get; private set; }

[JsonProperty("pullRequestId")]
public Int32 Id { get; private set; }

[JsonProperty("mergeStatus")]
public String MergeStatus { get; private set; }

[JsonProperty("sourceRefName")]
public String Source { get; private set; }

[JsonProperty("status")]
public String Status { get; private set; }

[JsonProperty("targetRefName")]
public String Target { get; private set; }

[JsonProperty("title")]
public String Title { get; private set; }

#endregion
}
}
88 changes: 79 additions & 9 deletions BuildsAppReborn.Access/TFS/TfsBuildProviderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@

namespace BuildsAppReborn.Access
{
internal abstract class TfsBuildProviderBase<TBuild, TBuildDefinition, TUser, TSourceVersion, TArtifact, TTestRun> : TfsBuildProviderBase, IBuildProvider
internal abstract class TfsBuildProviderBase<TBuild, TBuildDefinition, TUser, TSourceVersion, TArtifact, TTestRun, TPullRequest> : TfsBuildProviderBase, IBuildProvider
where TBuild : TfsBuild, new()
where TBuildDefinition : TfsBuildDefinition, new()
where TUser : TfsUser, new()
where TSourceVersion : TfsSourceVersion, new()
where TArtifact : TfsArtifact, new()
where TTestRun : TfsTestRun, new()
where TPullRequest : TfsPullRequest, new()
{
#region Implementation of IBuildProvider

Expand Down Expand Up @@ -58,7 +59,7 @@ public virtual async Task<DataResponse<IEnumerable<IBuild>>> GetBuilds(IEnumerab
var buildDefinitionsList = buildDefinitions.ToList();
if (!buildDefinitionsList.Any())
{
return new DataResponse<IEnumerable<IBuild>> {Data = Enumerable.Empty<IBuild>(), StatusCode = HttpStatusCode.NoContent};
return new DataResponse<IEnumerable<IBuild>> {Data = Enumerable.Empty<TBuild>(), StatusCode = HttpStatusCode.NoContent};
}

var projectUrl = settings.GetValueStrict<String>(ProjectUrlSettingKey).TrimEnd('/');
Expand All @@ -76,17 +77,53 @@ public virtual async Task<DataResponse<IEnumerable<IBuild>>> GetBuilds(IEnumerab
{
var result = await requestResponse.Content.ReadAsStringAsync();
var data = JsonConvert.DeserializeObject<List<TBuild>>(JObject.Parse(result)["value"].ToString());
data.Select(d => d.Definition).OfType<TBuildDefinition>().ToList().ForEach(d => d.BuildSettingsId = settings.UniqueId);
data.Select(d => d.Requester).OfType<TUser>().ToList().ForEach(a => a.ImageDataLoader = GetImageData(settings, a));

await ResolveSourceVersion(data, projectUrl, settings);
await ResolveArtifacts(data, projectUrl, settings);
await ResolveTestRuns(data, projectUrl, settings);
await ResolveAddtionalBuildData(settings, data, projectUrl);

return new DataResponse<IEnumerable<IBuild>> {Data = data, StatusCode = requestResponse.StatusCode};
}

return new DataResponse<IEnumerable<IBuild>> {Data = Enumerable.Empty<IBuild>(), StatusCode = requestResponse.StatusCode};
return new DataResponse<IEnumerable<IBuild>> {Data = Enumerable.Empty<TBuild>(), StatusCode = requestResponse.StatusCode};
}

throw new Exception($"Error while processing method!");
}

public virtual async Task<DataResponse<IEnumerable<IBuild>>> GetBuildsByPullRequests(BuildMonitorSettings settings)
{
var projectUrl = settings.GetValueStrict<String>(ProjectUrlSettingKey).TrimEnd('/');

var requestUrl = $"{projectUrl}/_apis/git/pullrequests?api-version={ApiVersion}";
var requestResponse = await GetRequestResponse(requestUrl, settings);
if (requestResponse.IsSuccessStatusCode)
{
var dict = new Dictionary<IPullRequest, IEnumerable<TBuild>>();

var result = await requestResponse.Content.ReadAsStringAsync();
var data = JsonConvert.DeserializeObject<List<TPullRequest>>(JObject.Parse(result)["value"].ToString());

foreach (var pullRequest in data)
{
var buildsResponse = await GetBuildsOfPullRequest(pullRequest, settings);
if (buildsResponse.IsSuccessStatusCode)
{
dict.Add(pullRequest, buildsResponse.Data);
}
else
{
throw new Exception($"Error while processing method!");
}
}

// sets the relation of the PR to the build object
foreach (var keyValuePair in dict)
{
foreach (var build in keyValuePair.Value)
{
build.PullRequest = keyValuePair.Key;
}
}

return new DataResponse<IEnumerable<IBuild>> {Data = dict.Values.SelectMany(a => a).ToList(), StatusCode = requestResponse.StatusCode};
}

throw new Exception($"Error while processing method!");
Expand Down Expand Up @@ -153,6 +190,29 @@ private static async Task<HttpResponseMessage> GetRequestResponse(String request

#region Private Methods

private async Task<DataResponse<IEnumerable<TBuild>>> GetBuildsOfPullRequest(IPullRequest pullRequest, BuildMonitorSettings settings)
{
var projectUrl = settings.GetValueStrict<String>(ProjectUrlSettingKey).TrimEnd('/');
var maxBuilds = settings.GetDefaultValueIfNotExists<Int32?>(MaxBuildsPerDefinitionSettingsKey);

// use fallback value when no value was defined via settings
if (!maxBuilds.HasValue) maxBuilds = 5;

var requestUrl = $"{projectUrl}/_apis/build/builds?api-version={ApiVersion}&branchName=refs%2Fpull%2F{pullRequest.Id}%2Fmerge&$top={maxBuilds}";
var requestResponse = await GetRequestResponse(requestUrl, settings);
if (requestResponse.IsSuccessStatusCode)
{
var result = await requestResponse.Content.ReadAsStringAsync();
var data = JsonConvert.DeserializeObject<List<TBuild>>(JObject.Parse(result)["value"].ToString());

await ResolveAddtionalBuildData(settings, data, projectUrl);

return new DataResponse<IEnumerable<TBuild>> {Data = data, StatusCode = requestResponse.StatusCode};
}

throw new Exception($"Error while processing method!");
}

private Tuple<String, String> GetGitOwnerAndRepo(String gitHubRepoUrl)
{
if (!String.IsNullOrWhiteSpace(gitHubRepoUrl))
Expand All @@ -173,6 +233,16 @@ private Tuple<String, String> GetGitOwnerAndRepo(String gitHubRepoUrl)
return null;
}

private async Task ResolveAddtionalBuildData(BuildMonitorSettings settings, List<TBuild> data, String projectUrl)
{
data.Select(d => d.Definition).OfType<TBuildDefinition>().ToList().ForEach(d => d.BuildSettingsId = settings.UniqueId);
data.Select(d => d.Requester).OfType<TUser>().ToList().ForEach(a => a.ImageDataLoader = GetImageData(settings, a));

await ResolveSourceVersion(data, projectUrl, settings);
await ResolveArtifacts(data, projectUrl, settings);
await ResolveTestRuns(data, projectUrl, settings);
}

private async Task ResolveArtifacts(IEnumerable<TBuild> builds, String projectUrl, BuildMonitorSettings settings)
{
foreach (var build in builds)
Expand Down
17 changes: 17 additions & 0 deletions BuildsAppReborn.Access/TFS2017/Models/Tfs2017PullRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using BuildsAppReborn.Contracts.Models;
using BuildsAppReborn.Infrastructure;
using Newtonsoft.Json;

namespace BuildsAppReborn.Access.Models
{
// ReSharper disable once UnusedMember.Global
internal class Tfs2017PullRequest : TfsPullRequest
{
#region Overrides of Base

[JsonConverter(typeof(InterfaceTypeConverter<Tfs2017User, IUser>))]
public override IUser CreatedBy { get; protected set; }

#endregion
}
}
2 changes: 1 addition & 1 deletion BuildsAppReborn.Access/TFS2017/Tfs2017BuildProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace BuildsAppReborn.Access
{
[BuildProviderExport(typeof(IBuildProvider), Id, Name, AuthenticationModes.Default | AuthenticationModes.AccessToken)]
[PartCreationPolicy(CreationPolicy.Shared)]
internal class Tfs2017BuildProvider : TfsBuildProviderBase<Tfs2017Build, Tfs2017BuildDefinition, Tfs2017User, Tfs2017SourceVersion, Tfs2017Artifact, Tfs2017TestRun>
internal class Tfs2017BuildProvider : TfsBuildProviderBase<Tfs2017Build, Tfs2017BuildDefinition, Tfs2017User, Tfs2017SourceVersion, Tfs2017Artifact, Tfs2017TestRun, Tfs2017PullRequest>
{
#region Overrides of Base

Expand Down
17 changes: 17 additions & 0 deletions BuildsAppReborn.Access/VSTS/Models/VstsPullRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using BuildsAppReborn.Contracts.Models;
using BuildsAppReborn.Infrastructure;
using Newtonsoft.Json;

namespace BuildsAppReborn.Access.Models
{
// ReSharper disable once UnusedMember.Global
internal class VstsPullRequest : TfsPullRequest
{
#region Overrides of Base

[JsonConverter(typeof(InterfaceTypeConverter<VstsUser, IUser>))]
public override IUser CreatedBy { get; protected set; }

#endregion
}
}
2 changes: 1 addition & 1 deletion BuildsAppReborn.Access/VSTS/VstsBuildProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace BuildsAppReborn.Access
{
[BuildProviderExport(typeof(IBuildProvider), Id, Name, AuthenticationModes.AccessToken)]
[PartCreationPolicy(CreationPolicy.Shared)]
internal class VstsBuildProvider : TfsBuildProviderBase<VstsBuild, VstsBuildDefinition, VstsUser, VstsSourceVersion, VstsArtifact, VstsTestRun>
internal class VstsBuildProvider : TfsBuildProviderBase<VstsBuild, VstsBuildDefinition, VstsUser, VstsSourceVersion, VstsArtifact, VstsTestRun, VstsPullRequest>
{
#region Overrides of Base

Expand Down
2 changes: 1 addition & 1 deletion BuildsAppReborn.Client/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ protected override void OnStartup(StartupEventArgs e)
this.globalSettingsContainer = compositionContainer.GetExportedValue<GlobalSettingsContainer>();

this.buildMonitor = compositionContainer.GetExportedValue<IBuildMonitorAdvanced>();
this.buildMonitor.Start(this.globalSettingsContainer.BuildMonitorSettingsContainer, TimeSpan.FromMinutes(1));
this.buildMonitor.Start(this.globalSettingsContainer.BuildMonitorSettingsContainer, this.globalSettingsContainer.GeneralSettings);

this.notifyIcon = (TaskbarIcon) FindResource("NotifyIcon");
if (this.notifyIcon != null)
Expand Down
1 change: 1 addition & 0 deletions BuildsAppReborn.Client/BuildsAppReborn.Client.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@
<Compile Include="Converter\BuildStatusToImageConverter.cs" />
<Compile Include="Converter\BuildStatusToSolidColorBrushForegroundConverter.cs" />
<Compile Include="Converter\BuildStatusToSolidColorBrushBackgroundConverter.cs" />
<Compile Include="Converter\EnumDescriptionConverter.cs" />
<Compile Include="Converter\CountToVisibilityConverter.cs" />
<Compile Include="Converter\NullVisibilityConverter.cs" />
<Compile Include="Converter\MinutesToTimeSpanConverter.cs" />
Expand Down
Loading

0 comments on commit b28ecd8

Please sign in to comment.