From f10d5b918a5f5746dc4724ea4cb7de15bc1da612 Mon Sep 17 00:00:00 2001 From: Igor Abade Date: Fri, 8 Apr 2022 21:01:27 -0300 Subject: [PATCH] Fix-team-cmdlets (#168) * 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 --- .../TfsCmdlets.Common/Services/IPaginator.cs | 14 +++++ .../Services/Impl/PaginatorImpl.cs | 54 +++++++++++++++++++ .../Generators/Controllers/ControllerInfo.cs | 3 +- .../Cmdlets/Git/GetGitRepository.cs | 2 +- .../Avatar/RemoveTeamProjectAvatar.cs | 7 ++- .../Git/Branch/RemoveGitBranchController.cs | 2 +- .../Git/RemoveGitRepositoryController.cs | 4 +- .../Identity/Group/RemoveGroupController.cs | 2 +- .../Group/RemoveGroupMemberController.cs | 5 +- ...RemoveReleaseDefinitionFolderController.cs | 5 +- .../Controllers/Team/GetTeamController.cs | 11 ++-- .../Controllers/Team/RemoveTeamController.cs | 4 +- .../RemoveTeamProjectAvatarController.cs | 6 +-- .../TeamProject/GetTeamProjectController.cs | 11 ++-- .../RemoveTeamProjectController.cs | 2 +- .../UndoTeamProjectRemovalController.cs | 2 + .../GetTeamProjectCollectionController.cs | 22 +++++++- .../RemoveTestPlanController.cs | 7 ++- .../Controllers/Wiki/RemoveWikiController.cs | 6 ++- .../WorkItem/RemoveWorkItemController.cs | 7 ++- .../WorkItem/UndoWorkItemRemovalController.cs | 2 +- Docs/ReleaseNotes/2.3.1.md | 13 +++++ RELEASENOTES.md | 34 ++++-------- psake.ps1 | 25 ++++++++- 24 files changed, 185 insertions(+), 65 deletions(-) create mode 100644 CSharp/TfsCmdlets.Common/Services/IPaginator.cs create mode 100644 CSharp/TfsCmdlets.Common/Services/Impl/PaginatorImpl.cs create mode 100644 Docs/ReleaseNotes/2.3.1.md diff --git a/CSharp/TfsCmdlets.Common/Services/IPaginator.cs b/CSharp/TfsCmdlets.Common/Services/IPaginator.cs new file mode 100644 index 000000000..8a237387c --- /dev/null +++ b/CSharp/TfsCmdlets.Common/Services/IPaginator.cs @@ -0,0 +1,14 @@ +using System.Management.Automation; +using System.Threading.Tasks; + +namespace TfsCmdlets.Services +{ + public interface IPaginator + { + IEnumerable Paginate(Func> enumerable, int pageSize = 100); + + IEnumerable Paginate(Func>> enumerable, string errorMessage, int pageSize = 100); + + // IEnumerable Paginate(Func>> enumerable, string errorMessage, int pageSize = 100); + } +} \ No newline at end of file diff --git a/CSharp/TfsCmdlets.Common/Services/Impl/PaginatorImpl.cs b/CSharp/TfsCmdlets.Common/Services/Impl/PaginatorImpl.cs new file mode 100644 index 000000000..5bb05ad68 --- /dev/null +++ b/CSharp/TfsCmdlets.Common/Services/Impl/PaginatorImpl.cs @@ -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 Paginate(Func>> 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 Paginate(Func> 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); + } + } + } +} diff --git a/CSharp/TfsCmdlets.SourceGenerators/Generators/Controllers/ControllerInfo.cs b/CSharp/TfsCmdlets.SourceGenerators/Generators/Controllers/ControllerInfo.cs index 17615793a..a593f97e4 100644 --- a/CSharp/TfsCmdlets.SourceGenerators/Generators/Controllers/ControllerInfo.cs +++ b/CSharp/TfsCmdlets.SourceGenerators/Generators/Controllers/ControllerInfo.cs @@ -173,8 +173,7 @@ private static IEnumerable 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 }; } diff --git a/CSharp/TfsCmdlets/Cmdlets/Git/GetGitRepository.cs b/CSharp/TfsCmdlets/Cmdlets/Git/GetGitRepository.cs index d8962454a..da8df3a8c 100644 --- a/CSharp/TfsCmdlets/Cmdlets/Git/GetGitRepository.cs +++ b/CSharp/TfsCmdlets/Cmdlets/Git/GetGitRepository.cs @@ -6,7 +6,7 @@ namespace TfsCmdlets.Cmdlets.Git /// /// Gets information from one or more Git repositories in a team project. /// - [TfsCmdlet(CmdletScope.Project, OutputType = typeof(GitRepository))] + [TfsCmdlet(CmdletScope.Project, OutputType = typeof(GitRepository), DefaultParameterSetName = "Get by ID or Name")] partial class GetGitRepository { /// diff --git a/CSharp/TfsCmdlets/Cmdlets/TeamProject/Avatar/RemoveTeamProjectAvatar.cs b/CSharp/TfsCmdlets/Cmdlets/TeamProject/Avatar/RemoveTeamProjectAvatar.cs index 8117d3e31..2ca4a9892 100644 --- a/CSharp/TfsCmdlets/Cmdlets/TeamProject/Avatar/RemoveTeamProjectAvatar.cs +++ b/CSharp/TfsCmdlets/Cmdlets/TeamProject/Avatar/RemoveTeamProjectAvatar.cs @@ -3,8 +3,13 @@ namespace TfsCmdlets.Cmdlets.TeamProject.Avatar /// /// Removes the team project avatar, resetting it to the default. /// - [TfsCmdlet(CmdletScope.Project, SupportsShouldProcess = true)] + [TfsCmdlet(CmdletScope.Collection, SupportsShouldProcess = true)] partial class RemoveTeamProjectAvatar { + /// + /// HELP_PARAM_PROJECT + /// + [Parameter(Mandatory = true, ValueFromPipeline = true)] + public object Project { get; set; } } } \ No newline at end of file diff --git a/CSharp/TfsCmdlets/Controllers/Git/Branch/RemoveGitBranchController.cs b/CSharp/TfsCmdlets/Controllers/Git/Branch/RemoveGitBranchController.cs index f2c86f749..346bb8b34 100644 --- a/CSharp/TfsCmdlets/Controllers/Git/Branch/RemoveGitBranchController.cs +++ b/CSharp/TfsCmdlets/Controllers/Git/Branch/RemoveGitBranchController.cs @@ -16,7 +16,7 @@ protected override IEnumerable Run() var projectId = commitUrl.Segments[commitUrl.Segments.Length - 7].Trim('/'); var repo = GetItem(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 { diff --git a/CSharp/TfsCmdlets/Controllers/Git/RemoveGitRepositoryController.cs b/CSharp/TfsCmdlets/Controllers/Git/RemoveGitRepositoryController.cs index a8614946e..e45731157 100644 --- a/CSharp/TfsCmdlets/Controllers/Git/RemoveGitRepositoryController.cs +++ b/CSharp/TfsCmdlets/Controllers/Git/RemoveGitRepositoryController.cs @@ -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(); } diff --git a/CSharp/TfsCmdlets/Controllers/Identity/Group/RemoveGroupController.cs b/CSharp/TfsCmdlets/Controllers/Identity/Group/RemoveGroupController.cs index cefed43ec..07fa5b4bd 100644 --- a/CSharp/TfsCmdlets/Controllers/Identity/Group/RemoveGroupController.cs +++ b/CSharp/TfsCmdlets/Controllers/Identity/Group/RemoveGroupController.cs @@ -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}'"); diff --git a/CSharp/TfsCmdlets/Controllers/Identity/Group/RemoveGroupMemberController.cs b/CSharp/TfsCmdlets/Controllers/Identity/Group/RemoveGroupMemberController.cs index 8bbca9dbc..355a30e3b 100644 --- a/CSharp/TfsCmdlets/Controllers/Identity/Group/RemoveGroupMemberController.cs +++ b/CSharp/TfsCmdlets/Controllers/Identity/Group/RemoveGroupMemberController.cs @@ -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}'"); diff --git a/CSharp/TfsCmdlets/Controllers/Pipeline/ReleaseManagement/RemoveReleaseDefinitionFolderController.cs b/CSharp/TfsCmdlets/Controllers/Pipeline/ReleaseManagement/RemoveReleaseDefinitionFolderController.cs index 461d902ba..04ef3404c 100644 --- a/CSharp/TfsCmdlets/Controllers/Pipeline/ReleaseManagement/RemoveReleaseDefinitionFolderController.cs +++ b/CSharp/TfsCmdlets/Controllers/Pipeline/ReleaseManagement/RemoveReleaseDefinitionFolderController.cs @@ -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) { diff --git a/CSharp/TfsCmdlets/Controllers/Team/GetTeamController.cs b/CSharp/TfsCmdlets/Controllers/Team/GetTeamController.cs index 86e0df09f..15ca5099d 100644 --- a/CSharp/TfsCmdlets/Controllers/Team/GetTeamController.cs +++ b/CSharp/TfsCmdlets/Controllers/Team/GetTeamController.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using Microsoft.TeamFoundation.Core.WebApi; using Microsoft.TeamFoundation.Core.WebApi.Types; using Microsoft.TeamFoundation.Work.WebApi; @@ -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() { @@ -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); diff --git a/CSharp/TfsCmdlets/Controllers/Team/RemoveTeamController.cs b/CSharp/TfsCmdlets/Controllers/Team/RemoveTeamController.cs index 336032227..050a70279 100644 --- a/CSharp/TfsCmdlets/Controllers/Team/RemoveTeamController.cs +++ b/CSharp/TfsCmdlets/Controllers/Team/RemoveTeamController.cs @@ -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}"); } diff --git a/CSharp/TfsCmdlets/Controllers/TeamProject/Avatar/RemoveTeamProjectAvatarController.cs b/CSharp/TfsCmdlets/Controllers/TeamProject/Avatar/RemoveTeamProjectAvatarController.cs index c1e3c990a..b98c1e8d5 100644 --- a/CSharp/TfsCmdlets/Controllers/TeamProject/Avatar/RemoveTeamProjectAvatarController.cs +++ b/CSharp/TfsCmdlets/Controllers/TeamProject/Avatar/RemoveTeamProjectAvatarController.cs @@ -7,17 +7,17 @@ partial class RemoveTeamProjectAvatarController { protected override IEnumerable Run() { - var tp = Data.GetProject(); var client = Data.GetClient(); - if (PowerShell.ShouldProcess(tp, $"Reset custom team project avatar image to default")) + foreach (var tp in Data.GetItems(new { Project = Parameters.Get(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; } } diff --git a/CSharp/TfsCmdlets/Controllers/TeamProject/GetTeamProjectController.cs b/CSharp/TfsCmdlets/Controllers/TeamProject/GetTeamProjectController.cs index c49c8a974..611eb0d05 100644 --- a/CSharp/TfsCmdlets/Controllers/TeamProject/GetTeamProjectController.cs +++ b/CSharp/TfsCmdlets/Controllers/TeamProject/GetTeamProjectController.cs @@ -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(); @@ -83,9 +89,6 @@ private WebApiTeamProject FetchProject(string project, ProjectHttpClient client, => client.GetProject(project, includeDetails).GetResult($"Error getting team project '{project}'"); private IEnumerable 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)")); } } diff --git a/CSharp/TfsCmdlets/Controllers/TeamProject/RemoveTeamProjectController.cs b/CSharp/TfsCmdlets/Controllers/TeamProject/RemoveTeamProjectController.cs index 7790574b9..6bb2adef1 100644 --- a/CSharp/TfsCmdlets/Controllers/TeamProject/RemoveTeamProjectController.cs +++ b/CSharp/TfsCmdlets/Controllers/TeamProject/RemoveTeamProjectController.cs @@ -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; diff --git a/CSharp/TfsCmdlets/Controllers/TeamProject/UndoTeamProjectRemovalController.cs b/CSharp/TfsCmdlets/Controllers/TeamProject/UndoTeamProjectRemovalController.cs index c9d0ff159..f3d73c2d0 100644 --- a/CSharp/TfsCmdlets/Controllers/TeamProject/UndoTeamProjectRemovalController.cs +++ b/CSharp/TfsCmdlets/Controllers/TeamProject/UndoTeamProjectRemovalController.cs @@ -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}", diff --git a/CSharp/TfsCmdlets/Controllers/TeamProjectCollection/GetTeamProjectCollectionController.cs b/CSharp/TfsCmdlets/Controllers/TeamProjectCollection/GetTeamProjectCollectionController.cs index 8f72e2cb7..c4f8b4bce 100644 --- a/CSharp/TfsCmdlets/Controllers/TeamProjectCollection/GetTeamProjectCollectionController.cs +++ b/CSharp/TfsCmdlets/Controllers/TeamProjectCollection/GetTeamProjectCollectionController.cs @@ -3,7 +3,7 @@ namespace TfsCmdlets.Controllers.TeamProjectCollection { [CmdletController(typeof(Connection))] - partial class GetTeamProjectCollectionController + partial class GetTeamProjectCollectionController { [Import] private ICurrentConnections CurrentConnections { get; } @@ -18,7 +18,25 @@ protected override IEnumerable Run() yield break; } - yield return Data.GetCollection(new { Collection = Collection ?? Parameters.Get("Organization") }); + var colsObj = Parameters.HasParameter("Organization") ? + Parameters.Get("Organization") : + Parameters.Get("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 }); + } } } } \ No newline at end of file diff --git a/CSharp/TfsCmdlets/Controllers/TestManagement/RemoveTestPlanController.cs b/CSharp/TfsCmdlets/Controllers/TestManagement/RemoveTestPlanController.cs index 7699b9dc9..9b2e5ac9d 100644 --- a/CSharp/TfsCmdlets/Controllers/TestManagement/RemoveTestPlanController.cs +++ b/CSharp/TfsCmdlets/Controllers/TestManagement/RemoveTestPlanController.cs @@ -10,21 +10,20 @@ protected override IEnumerable Run() { var plans = Data.GetItems(); var force = Parameters.Get(nameof(RemoveTestPlan.Force)); - var tp = Data.GetProject(); var client = Data.GetClient(); 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; diff --git a/CSharp/TfsCmdlets/Controllers/Wiki/RemoveWikiController.cs b/CSharp/TfsCmdlets/Controllers/Wiki/RemoveWikiController.cs index e65f3574a..be1ec0939 100644 --- a/CSharp/TfsCmdlets/Controllers/Wiki/RemoveWikiController.cs +++ b/CSharp/TfsCmdlets/Controllers/Wiki/RemoveWikiController.cs @@ -8,7 +8,6 @@ partial class RemoveWikiController { protected override IEnumerable Run() { - var tp = Data.GetProject(); var client = Data.GetClient(); var wikis = Data.GetItems(); @@ -16,7 +15,10 @@ protected override IEnumerable Run() 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) diff --git a/CSharp/TfsCmdlets/Controllers/WorkItem/RemoveWorkItemController.cs b/CSharp/TfsCmdlets/Controllers/WorkItem/RemoveWorkItemController.cs index f48d758f2..d83722d05 100644 --- a/CSharp/TfsCmdlets/Controllers/WorkItem/RemoveWorkItemController.cs +++ b/CSharp/TfsCmdlets/Controllers/WorkItem/RemoveWorkItemController.cs @@ -10,18 +10,17 @@ protected override IEnumerable Run() { var wis = Data.GetItems(); var tpc = Data.GetCollection(); - var tp = Data.GetProject(); var destroy = Parameters.Get(nameof(RemoveWorkItem.Destroy)); var force = Parameters.Get(nameof(RemoveWorkItem.Force)); var client = Data.GetClient(); 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}"); } diff --git a/CSharp/TfsCmdlets/Controllers/WorkItem/UndoWorkItemRemovalController.cs b/CSharp/TfsCmdlets/Controllers/WorkItem/UndoWorkItemRemovalController.cs index bc600653c..d27a9efd3 100644 --- a/CSharp/TfsCmdlets/Controllers/WorkItem/UndoWorkItemRemovalController.cs +++ b/CSharp/TfsCmdlets/Controllers/WorkItem/UndoWorkItemRemovalController.cs @@ -11,7 +11,7 @@ protected override IEnumerable Run() foreach (var wi in GetItems(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() { diff --git a/Docs/ReleaseNotes/2.3.1.md b/Docs/ReleaseNotes/2.3.1.md new file mode 100644 index 000000000..a17a41716 --- /dev/null +++ b/Docs/ReleaseNotes/2.3.1.md @@ -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. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fca70237a..a58c8e4f2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,37 +1,25 @@ # TfsCmdlets Release Notes -## Version 2.3.0 (_03/Apr/2022_) +## Version 2.3.1 (_08/Apr/2022_) -This release adds initial support for Azure Artifacts and fixes a few bugs related to team membership handling. - -## New cmdlets - -### Artifacts - -* [`Get-TfsArtifact`](https://tfscmdlets.dev/docs/cmdlets/Artifact/Get-TfsArtifact) -* [`Get-TfsArtifactFeed`](https://tfscmdlets.dev/docs/cmdlets/Artifact/Get-TfsArtifactFeed) -* [`Get-TfsArtifactVersion`](https://tfscmdlets.dev/docs/cmdlets/Artifact/Get-TfsArtifactVersion) -* [`Get-TfsArtifactFeedView`](https://tfscmdlets.dev/docs/cmdlets/Artifact/Get-TfsArtifactFeedView) - -### Git - -* [`Remove-TfsGitBranch`](https://tfscmdlets.dev/docs/cmdlets/Git/Branch/Remove-TfsGitBranch) - -## Improvements - -* Add `-AreaPaths` argument to `New-TfsTeam` so team area paths can be defined at team creation time. -* `Get-TfsWorkItem` supports long result sets again. In the previous release, query results were limited to 200 work items due to a limitation in the Azure DevOps "Get Work Items Batch" API. In this version we added back the original behavior as a fallback logic: to fetch work items one at a time to circumvent the limitation. Though slower, it can fetch any number of work items (fixes [#164](https://github.com/igoravl/TfsCmdlets/issues/164)). +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 -* `Add-TfsTeamAdmin` limited team administrators to users (groups could not be added as admins), even though Azure DevOps supports it. This release lifts this restriction. -* Fixes a bug introduced in the last version where `Get-TfsTeamMember` and `Get-TfsTeamAdmin` would not return group members -* Under some circumstances `New-TfsTeam` would not respect `-NoDefaultArea` and `-NoBacklogIteration` +* `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. ----------------------- ## Previous Versions +### Version 2.3.0 (_04/Mar/2022_) + +See release notes [here](Docs/ReleaseNotes/2.3.0.md). + ### Version 2.2.1 (_10/Feb/2022_) See release notes [here](Docs/ReleaseNotes/2.2.1.md). diff --git a/psake.ps1 b/psake.ps1 index 0cae5728d..675ff2466 100644 --- a/psake.ps1 +++ b/psake.ps1 @@ -59,7 +59,7 @@ Properties { Task Rebuild -Depends Clean, Build { } -Task Package -Depends Build, AllTests, RemoveEmptyFolders, PackageNuget, PackageChocolatey, PackageMSI, PackageWinget, PackageDocs, PackageModule { +Task Package -Depends Build, AllTests, RemoveEmptyFolders, ValidateReleaseNotes, PackageNuget, PackageChocolatey, PackageMSI, PackageWinget, PackageDocs, PackageModule { } Task Build -Depends CleanOutputDir, CreateOutputDir, BuildLibrary, UnitTests, GenerateHelp, CopyFiles, GenerateTypesXml, GenerateFormatXml, GenerateNestedModule, UpdateModuleManifest, GenerateDocs { @@ -279,6 +279,29 @@ Task Clean { } +Task ValidateReleaseNotes { + + $path = Join-Path $RootProjectDir "Docs/ReleaseNotes/${ThreePartVersion}.md" + + if(-not (Test-Path $path -PathType Leaf)) { + throw "Release notes file '$path' not found" + } + + $releaseNotes = Get-Content $path -Encoding utf8 + $topVersionLine = $releaseNotes[2] + + if($topVersionLine -notlike "*$ThreePartVersion*") { + throw "File '$path' does not contain the release notes for version $threePartVersion." + } + + $releaseNotes = (Get-Content (Join-Path $RootProjectDir 'RELEASENOTES.md') -Encoding utf8) + $topVersionLine = $releaseNotes[2] + + if($topVersionLine -notmatch "$ThreePartVersion") { + throw "File 'RELEASENOTES.md' does not contain the release notes for version $threePartVersion" + } +} + Task PackageModule -Depends Build { if (-not (Test-Path $PortableDir -PathType Container)) { New-Item $PortableDir -ItemType Directory -Force | Out-Null }