Skip to content

Commit

Permalink
Fix-team-cmdlets (#168)
Browse files Browse the repository at this point in the history
* Fix the handling of team project references

* Add paginator service

* Disable caching for scope properties

* Use paginator

* Add missing parameterset annotation

* Fix pipeline handling

* Improve ShouldProcess support

* Fixes collection parameter handling

* Validate release notes

* Update release notes

* Remove unused field

* Update release notes
  • Loading branch information
igoravl authored Apr 9, 2022
1 parent 63c04ab commit f10d5b9
Show file tree
Hide file tree
Showing 24 changed files with 185 additions and 65 deletions.
14 changes: 14 additions & 0 deletions CSharp/TfsCmdlets.Common/Services/IPaginator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.Management.Automation;
using System.Threading.Tasks;

namespace TfsCmdlets.Services
{
public interface IPaginator
{
IEnumerable<T> Paginate<T>(Func<int, int, IEnumerable<T>> enumerable, int pageSize = 100);

IEnumerable<T> Paginate<T>(Func<int, int, Task<IEnumerable<T>>> enumerable, string errorMessage, int pageSize = 100);

// IEnumerable<T> Paginate<T>(Func<int, int, Task<List<T>>> enumerable, string errorMessage, int pageSize = 100);
}
}
54 changes: 54 additions & 0 deletions CSharp/TfsCmdlets.Common/Services/Impl/PaginatorImpl.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Threading.Tasks;
using Microsoft.TeamFoundation.Core.WebApi;
using TfsCmdlets.Models;

namespace TfsCmdlets.Services.Impl
{
[Export(typeof(IPaginator))]
public class Paginator : IPaginator
{
public IEnumerable<T> Paginate<T>(Func<int, int, Task<IEnumerable<T>>> enumerable, string errorMessage, int pageSize)
{
var isReturning = true;
var loop = 0;
int items;

while (isReturning)
{
isReturning = false;
items = 0;

foreach (var result in enumerable(pageSize, loop++ * pageSize).GetResult(errorMessage))
{
items++;
isReturning = true;
yield return result;
}

isReturning = isReturning && (items == pageSize);
}
}

public IEnumerable<T> Paginate<T>(Func<int, int, IEnumerable<T>> enumerable, int pageSize)
{
var isReturning = true;
var loop = 0;
int items;

while (isReturning)
{
isReturning = false;
items = 0;

foreach (var result in enumerable(pageSize, loop++ * pageSize))
{
items++;
isReturning = true;
yield return result;
}

isReturning = isReturning && (items == pageSize);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,7 @@ private static IEnumerable<GeneratedProperty> GenerateScopeProperty(CmdletScope
{
yield return new GeneratedProperty(scope.ToString(), "object", $@" // {scope}
protected bool Has_{scope} => Parameters.HasParameter(""{scope}"");
private {scopeType} _{scope};
protected {scopeType} {scope} => _{scope} ??= Data.Get{scope}();
protected {scopeType} {scope} => Data.Get{scope}();
") { IsScope = true };
}
Expand Down
2 changes: 1 addition & 1 deletion CSharp/TfsCmdlets/Cmdlets/Git/GetGitRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace TfsCmdlets.Cmdlets.Git
/// <summary>
/// Gets information from one or more Git repositories in a team project.
/// </summary>
[TfsCmdlet(CmdletScope.Project, OutputType = typeof(GitRepository))]
[TfsCmdlet(CmdletScope.Project, OutputType = typeof(GitRepository), DefaultParameterSetName = "Get by ID or Name")]
partial class GetGitRepository
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,13 @@ namespace TfsCmdlets.Cmdlets.TeamProject.Avatar
/// <summary>
/// Removes the team project avatar, resetting it to the default.
/// </summary>
[TfsCmdlet(CmdletScope.Project, SupportsShouldProcess = true)]
[TfsCmdlet(CmdletScope.Collection, SupportsShouldProcess = true)]
partial class RemoveTeamProjectAvatar
{
/// <summary>
/// HELP_PARAM_PROJECT
/// </summary>
[Parameter(Mandatory = true, ValueFromPipeline = true)]
public object Project { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ protected override IEnumerable Run()
var projectId = commitUrl.Segments[commitUrl.Segments.Length - 7].Trim('/');
var repo = GetItem<GitRepository>(new { Repository = repoId, Project = projectId });

if (!PowerShell.ShouldProcess($"Git repository '{repo.Name}'", $"Delete branch '{branch.Name}'")) continue;
if (!PowerShell.ShouldProcess($"[Project: {repo.ProjectReference.Name}]/[Repository: {repo.Name}]/[Branch: {branch.Name}]", $"Delete branch")) continue;

try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ protected override IEnumerable Run()

foreach (var repo in Items)
{
if (!PowerShell.ShouldProcess(Project, $"Delete Git repository '{repo.Name}'")) continue;
if (!PowerShell.ShouldProcess($"[Project: {repo.ProjectReference.Name}]/[Repository: {repo.Name}]", $"Delete repository")) continue;

if (!Force && !PowerShell.ShouldContinue($"Are you sure you want to delete Git repository '{repo.Name}'?")) continue;
if (!(repo.DefaultBranch == null || Force) && !PowerShell.ShouldContinue($"Are you sure you want to delete Git repository '{repo.Name}'?")) continue;

client.DeleteRepositoryAsync(repo.Id).Wait();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ protected override IEnumerable Run()

foreach (var group in Items)
{
if (!PowerShell.ShouldProcess($"Group '{group.PrincipalName}'", "Remove group")) continue;
if (!PowerShell.ShouldProcess(group.PrincipalName, "Remove group")) continue;

client.DeleteGroupAsync(group.Descriptor)
.Wait($"Error removing group '{group.PrincipalName}'");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ protected override IEnumerable Run()

Logger.Log($"Adding {m.IdentityType} '{m.DisplayName} ({m.UniqueName})' to group '{g.DisplayName}'");

if (!PowerShell.ShouldProcess($"Group '{g.DisplayName}'",
$"Remove member '{m.DisplayName} ({m.UniqueName})'")) return null;
if (!PowerShell.ShouldProcess($"[Group: {g.DisplayName}]/[Member: '{m.DisplayName} ({m.UniqueName})']", "Remove member")) return null;

Logger.Log($"Removing '{m.DisplayName} ({m.UniqueName}))' from group '{g.DisplayName}'");
Logger.Log($"Removing '{m.DisplayName} ({m.UniqueName})' from group '{g.DisplayName}'");

client.RemoveMemberFromGroupAsync(g.Descriptor, m.Descriptor)
.GetResult($"Error removing '{m.DisplayName} ({m.UniqueName}))' from group '{g.DisplayName}'");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ protected override IEnumerable Run()

foreach (var f in folders)
{
if (!PowerShell.ShouldProcess(tp, $"Remove release folder '{f.Path}'"))
{
continue;
}
if (!PowerShell.ShouldProcess($"[Project: {tp.Name}]/[Folder: {f.Path}]", "Remove release definition folder")) continue;

if (!recurse)
{
Expand Down
11 changes: 8 additions & 3 deletions CSharp/TfsCmdlets/Controllers/Team/GetTeamController.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Threading.Tasks;
using Microsoft.TeamFoundation.Core.WebApi;
using Microsoft.TeamFoundation.Core.WebApi.Types;
using Microsoft.TeamFoundation.Work.WebApi;
Expand All @@ -9,7 +10,10 @@ namespace TfsCmdlets.Controllers.Team
partial class GetTeamController
{
[Import]
public ICurrentConnections CurrentConnections { get; }
private ICurrentConnections CurrentConnections { get; }

[Import]
private IPaginator Paginator { get; }

protected override IEnumerable Run()
{
Expand Down Expand Up @@ -76,8 +80,9 @@ protected override IEnumerable Run()
}
case string s:
{
foreach (var result in client.GetTeamsAsync(Project.Name)
.GetResult($"Error getting team(s) '{s}'")

foreach (var result in Paginator.Paginate(
(top, skip) => client.GetTeamsAsync(Project.Name, top: top, skip: skip).GetResult($"Error getting team(s) '{s}'"))
.Where(t => t.Name.IsLike(s)))
{
yield return CreateTeamObject(result);
Expand Down
4 changes: 2 additions & 2 deletions CSharp/TfsCmdlets/Controllers/Team/RemoveTeamController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ protected override IEnumerable Run()

foreach (var team in Items)
{
if (!PowerShell.ShouldProcess(Project, $"Delete team '{team.Name}'")) continue;
if (!PowerShell.ShouldProcess($"[Project: {team.ProjectName}]/[Team: {team.Name}]", $"Delete team")) continue;

client.DeleteTeamAsync(Project.Name, team.Name)
client.DeleteTeamAsync(team.ProjectName, team.Name)
.Wait($"Error deleting team {team.Name}");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ partial class RemoveTeamProjectAvatarController
{
protected override IEnumerable Run()
{
var tp = Data.GetProject();
var client = Data.GetClient<ProjectHttpClient>();

if (PowerShell.ShouldProcess(tp, $"Reset custom team project avatar image to default"))
foreach (var tp in Data.GetItems<WebApiTeamProject>(new { Project = Parameters.Get<object>(nameof(Project)) }))
{
if (!PowerShell.ShouldProcess($"[Project: {tp.Name}]", $"Remove custom team project avatar")) continue;

Logger.Log($"Resetting team project avatar image to default");

client.RemoveProjectAvatarAsync(tp.Name)
.Wait("Error removing project avatar");
}

return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ namespace TfsCmdlets.Controllers.TeamProject
[CmdletController(typeof(WebApiTeamProject))]
partial class GetTeamProjectController
{
[Import]
private ICurrentConnections CurrentConnections { get; }

[Import]
private IPaginator Paginator { get; }

protected override IEnumerable Run()
{
var client = GetClient<ProjectHttpClient>();
Expand Down Expand Up @@ -83,9 +89,6 @@ private WebApiTeamProject FetchProject(string project, ProjectHttpClient client,
=> client.GetProject(project, includeDetails).GetResult($"Error getting team project '{project}'");

private IEnumerable<TeamProjectReference> FetchProjects(ProjectState stateFilter, ProjectHttpClient client)
=> client.GetProjects(stateFilter).GetResult($"Error getting team project(s)");

[Import]
private ICurrentConnections CurrentConnections { get; }
=> Paginator.Paginate((top, skip) => client.GetProjects(stateFilter, top: top, skip: skip).GetResult($"Error getting team project(s)"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ protected override IEnumerable Run()

foreach (var tp in tps)
{
if (!PowerShell.ShouldProcess(tpc, $"Delete team project '{tp.Name}'")) continue;
if (!PowerShell.ShouldProcess($"[Organization: {tpc.DisplayName}]/[Project: {tp.Name}]", "Delete team project")) continue;

if (!force && !PowerShell.ShouldContinue($"Are you sure you want to delete team project '{tp.Name}'?")) continue;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ protected override IEnumerable Run()

foreach (var tp in references)
{
if (!PowerShell.ShouldProcess($"[Organization: {Collection.DisplayName}]/[Project: {tp.Name}]", "Restore deleted team project")) continue;

RestApiService.InvokeAsync(
Data.GetCollection(),
$"/_apis/projects/{tp.Id}",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace TfsCmdlets.Controllers.TeamProjectCollection
{
[CmdletController(typeof(Connection))]
partial class GetTeamProjectCollectionController
partial class GetTeamProjectCollectionController
{
[Import]
private ICurrentConnections CurrentConnections { get; }
Expand All @@ -18,7 +18,25 @@ protected override IEnumerable Run()
yield break;
}

yield return Data.GetCollection(new { Collection = Collection ?? Parameters.Get<object>("Organization") });
var colsObj = Parameters.HasParameter("Organization") ?
Parameters.Get<object>("Organization") :
Parameters.Get<object>("Collection");

IEnumerable cols;

if (colsObj is ICollection enumObj)
{
cols = enumObj;
}
else
{
cols = new[] { colsObj };
}

foreach (var col in cols)
{
yield return Data.GetCollection(new { Collection = col });
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,20 @@ protected override IEnumerable Run()
{
var plans = Data.GetItems<TestPlan>();
var force = Parameters.Get<bool>(nameof(RemoveTestPlan.Force));
var tp = Data.GetProject();
var client = Data.GetClient<TestPlanHttpClient>();

foreach (var plan in plans)
{
if (!PowerShell.ShouldProcess(tp, $"Delete test plan '{plan.Name}'")) continue;
if (!PowerShell.ShouldProcess($"[Project: {plan.Project.Name}]/[Plan: #{plan.Id} ({plan.Name})]", $"Delete test plan")) continue;

var suites = client.GetTestSuitesForPlanAsync(tp.Name, plan.Id, SuiteExpand.Children)
var suites = client.GetTestSuitesForPlanAsync(plan.Project.Name, plan.Id, SuiteExpand.Children)
.GetResult($"Error retrieving test suites for test plan '{plan.Name}'");

var hasChildren = (suites.Count > 1 || suites[0].Children?.Count > 0);

if (hasChildren && !force & !PowerShell.ShouldContinue($"Are you sure you want to delete test plan '{plan.Name}' and all of its contents?")) continue;

client.DeleteTestPlanAsync(tp.Name, plan.Id)
client.DeleteTestPlanAsync(plan.Project.Name, plan.Id)
.Wait($"Error deleting test plan '{plan.Name}'");
}
return null;
Expand Down
6 changes: 4 additions & 2 deletions CSharp/TfsCmdlets/Controllers/Wiki/RemoveWikiController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ partial class RemoveWikiController
{
protected override IEnumerable Run()
{
var tp = Data.GetProject();
var client = Data.GetClient<WikiHttpClient>();

var wikis = Data.GetItems<WikiV2>();
var force = Parameters.Get<bool>("Force");

foreach (var w in wikis)
{
if (!PowerShell.ShouldProcess(tp, $"Remove wiki '{w.Name}'")) continue;
var tp = Data.GetProject(new { Project = w.ProjectId });

if (!PowerShell.ShouldProcess($"[Project: {tp.Name}]/[Wiki: {w.Name}]", $"Remove wiki '{w.Name}'")) continue;

if (!force && !PowerShell.ShouldContinue($"Are you sure you want to delete wiki '{w.Name}'?")) continue;

if (w.Type == WikiType.ProjectWiki)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,17 @@ protected override IEnumerable Run()
{
var wis = Data.GetItems<WebApiWorkItem>();
var tpc = Data.GetCollection();
var tp = Data.GetProject();
var destroy = Parameters.Get<bool>(nameof(RemoveWorkItem.Destroy));
var force = Parameters.Get<bool>(nameof(RemoveWorkItem.Force));
var client = Data.GetClient<WorkItemTrackingHttpClient>();

foreach (var wi in wis)
{
if (!PowerShell.ShouldProcess(tpc, $"{(destroy ? "Destroy" : "Delete")} work item {wi.Id}")) continue;
if (!PowerShell.ShouldProcess($"[Organization: {tpc.DisplayName}]/[Work Item: {wi.Id}]", $"{(destroy ? "Destroy" : "Delete")} work item")) continue;

if (destroy && !(force || PowerShell.ShouldContinue("Are you sure you want to destroy work item {wi.id}?"))) continue;
if (destroy && !(force || PowerShell.ShouldContinue($"Are you sure you want to destroy work item {wi.Id}?"))) continue;

client.DeleteWorkItemAsync(tp.Name, (int)wi.Id, destroy)
client.DeleteWorkItemAsync((int)wi.Id, destroy)
.GetResult($"Error {(destroy ? "destroying" : "deleting")} work item {wi.Id}");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ protected override IEnumerable Run()

foreach (var wi in GetItems<WebApiWorkItem>(new { Deleted = true }))
{
if (!PowerShell.ShouldProcess(Collection, $"Restore {wi.Fields["System.WorkItemType"]} #{wi.Id} ('{wi.Fields["System.Title"]}')")) continue;
if (!PowerShell.ShouldProcess($"[Organization: {Collection.DisplayName}]/[Work Item: {wi.Id}]", $"Restore work item")) continue;

client.RestoreWorkItemAsync(new Microsoft.TeamFoundation.WorkItemTracking.WebApi.Models.WorkItemDeleteUpdate()
{
Expand Down
13 changes: 13 additions & 0 deletions Docs/ReleaseNotes/2.3.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# TfsCmdlets Release Notes

## Version 2.3.1 (_07/Apr/2022_)

This release brings a few minor fixes to Team cmdlets and to pipeline handling. No new features and/or cmdlets have been introduced in this version.

## Fixes

* `Get-TfsTeam` and `Get-TfsTeamProject` were limited to a maximum of 100 results. This has been fixed. Now they will return all results.
* Under certain circumstances, `Get-TfsTeamProjectCollection` (and, by extension, Get-TfsOrganization) would throw an error with the message "_Invalid or non-existent Collection System.Object[]._" (fixes [#165](https://github.com/igoravl/TfsCmdlets/issues/165))
* Fixes a caching bug in the handling of the -Project parameter that could lead to the wrong project being returned.
* Fixes pipelining bugs in several cmdlets (most noticeably `Get-TfsReposity`, which wouldn't work when connected to a pipeline).
* Improves the readability of ShouldProcess (Confirm / WhatIf) output in several cmdlets.
Loading

0 comments on commit f10d5b9

Please sign in to comment.