Skip to content

Commit a8ad84b

Browse files
authored
Fix bugs in Get-TfsWorkItem, Get-TfsArea, Get-TfsIteration, Invoke-TfsRestApi, New-TfsTeam and Set-TfsTeam. (#214)
* Update VS Code settings * fix: Improve handling of response when content-type is missing * chore: Improve readability * fix: Deal with team projects with reserved regex chars * fix: Issue "Get-TfsWorkItem : Value cannot be null. Parameter name: values" when specifying -Fields '*' #211 * fix: Handling of node paths in Get-TfsWorkItem * Update release notes * Update release notes * Update release notes * chore: Update CodeQL action to v3 * Add SECURITY.md * chore: Update SECURITY.md * fix: Adjust syntax for building in PS Core * chore: Update tests * fix: Wildcard handling * chore: Update security report link * chore: Upgrade WiX to v5 * fix: Correct variable name * fix: Typo * chore: Simplify WiX code * chore: Update release notes
1 parent 276cfc9 commit a8ad84b

File tree

16 files changed

+220
-275
lines changed

16 files changed

+220
-275
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
with:
2727
fetch-depth: 0
2828
- name: Initialize CodeQL
29-
uses: github/codeql-action/init@v2
29+
uses: github/codeql-action/init@v3
3030
with:
3131
languages: csharp
3232
- name: Build module
@@ -35,7 +35,7 @@ jobs:
3535
run: |
3636
./Build.ps1 -Targets Package -Config ${{ env.Config }} -Verbose:$${{ env.Debug }} -SkipReleaseNotes:$${{ env.SkipReleaseNotes }}
3737
- name: Perform CodeQL Analysis
38-
uses: github/codeql-action/analyze@v2
38+
uses: github/codeql-action/analyze@v3
3939
- name: Publish Nuget
4040
uses: actions/upload-artifact@v3
4141
with:

.vscode/launch.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@
4747
"Import-Module ${workspaceFolder}/out/module/TfsCmdlets.psd1; Enter-TfsShell"
4848
],
4949
"cwd": "${workspaceFolder}",
50-
"console": "externalTerminal",
51-
"preLaunchTask": "Build"
50+
"console": "externalTerminal"
5251
}
5352
]
5453
}

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,6 @@
4040
"titleBar.inactiveBackground": "#4b008299",
4141
"titleBar.inactiveForeground": "#e7e7e799"
4242
},
43-
"peacock.color": "Indigo"
43+
"peacock.color": "Indigo",
44+
"dotnet.preferCSharpExtension": true
4445
}

CSharp/TfsCmdlets.Common/Services/Impl/NodeUtilImpl.cs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ public string NormalizeNodePath(string path, string projectName = "", string sco
1010
bool includeTrailingSeparator = false, bool includeTeamProject = false, char separator = '\\')
1111
{
1212
if (path == null) throw new ArgumentNullException("path");
13-
//if (projectName == null) throw new ArgumentNullException("projectName");
1413
if (includeTeamProject && string.IsNullOrEmpty(projectName)) throw new ArgumentNullException("projectName");
1514
if (includeScope && string.IsNullOrEmpty(scope)) throw new ArgumentNullException("scope");
1615
if (excludePath && !includeScope && !includeTeamProject) throw new ArgumentException("excludePath is only valid when either includeScope or includeTeamProject are true");
@@ -29,28 +28,28 @@ public string NormalizeNodePath(string path, string projectName = "", string sco
2928

3029
if (!excludePath)
3130
{
32-
if (path.Equals(projectName) || path.StartsWith($@"{projectName}{separator}"))
31+
if (path.Equals(projectName, StringComparison.OrdinalIgnoreCase) || path.StartsWith($@"{projectName}{separator}", StringComparison.OrdinalIgnoreCase))
3332
{
34-
if (Regex.IsMatch(path, $@"^{projectName}\{separator}{scope}\{separator}"))
35-
{
36-
path = path.Substring($"{projectName}{separator}{scope}{separator}".Length);
37-
}
38-
if (Regex.IsMatch(path, $@"^{projectName}\{separator}"))
39-
{
40-
path = path.Substring($"{projectName}{separator}".Length);
41-
}
42-
else if (path.Equals(projectName, StringComparison.OrdinalIgnoreCase))
33+
if (path.Equals(projectName, StringComparison.OrdinalIgnoreCase))
4334
{
4435
path = "";
4536
}
37+
else{
38+
var escapedProject = Regex.Escape(projectName);
39+
var escapedScope = Regex.Escape(scope);
40+
var escapedSep = Regex.Escape(separator.ToString());
41+
var pattern = $@"^{escapedProject}{escapedSep}({escapedScope}{separator}?)?";
42+
43+
path = Regex.Replace(path, pattern, "");
44+
}
4645
}
47-
else if (path.Equals(scope) || path.StartsWith($"{scope}{separator}"))
46+
else if (path.Equals(scope, StringComparison.OrdinalIgnoreCase) || path.StartsWith($"{scope}{separator}", StringComparison.OrdinalIgnoreCase))
4847
{
49-
if (Regex.IsMatch(path, $@"^{scope}\{separator}"))
48+
if(path.Length > scope.Length)
5049
{
5150
path = path.Substring(path.IndexOf(separator) + 1);
5251
}
53-
else if (path.Equals(scope, StringComparison.OrdinalIgnoreCase))
52+
else
5453
{
5554
path = "";
5655
}

CSharp/TfsCmdlets/Controllers/RestApi/InvokeRestApiController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ protected override IEnumerable Run()
132132
yield break;
133133
}
134134

135-
var responseType = result.Content.Headers.ContentType.MediaType;
135+
var responseType = result.Content.Headers.ContentType?.MediaType;
136136

137137
switch (responseType)
138138
{

CSharp/TfsCmdlets/Controllers/Team/SetTeamController.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ protected override IEnumerable Run()
8282
if (usesAreaPath)
8383
{
8484
Logger.Log("Treating Team Field Value as Area Path");
85-
DefaultAreaPath = NodeUtil.NormalizeNodePath(DefaultAreaPath, Project.Name, "Areas", includeTeamProject: true);
85+
DefaultAreaPath = NodeUtil.NormalizeNodePath(DefaultAreaPath, Project.Name, "Areas",
86+
includeTeamProject: true,
87+
includeLeadingSeparator: true);
8688

8789
var a = new { Node = DefaultAreaPath, StructureGroup = TreeStructureGroup.Areas };
8890

CSharp/TfsCmdlets/Controllers/WorkItem/AreasIterations/GetClassificationNodeController.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,22 @@ protected override IEnumerable Run()
3636
}
3737
case string s when !string.IsNullOrEmpty(s) && s.IsWildcard():
3838
{
39-
path = NodeUtil.NormalizeNodePath(s, tp.Name, structureGroup.ToString().TrimEnd('s'), true, false, true, false, true);
39+
path = NodeUtil.NormalizeNodePath(s, tp.Name, structureGroup.ToString().TrimEnd('s'),
40+
includeScope: true,
41+
excludePath: false,
42+
includeLeadingSeparator: true,
43+
includeTrailingSeparator: false,
44+
includeTeamProject: true);
4045
break;
4146
}
4247
case string s when !string.IsNullOrEmpty(s):
4348
{
44-
path = NodeUtil.NormalizeNodePath(s, tp.Name, structureGroup.ToString().TrimEnd('s'), false, false, true, false, false);
49+
path = NodeUtil.NormalizeNodePath(s, tp.Name, structureGroup.ToString().TrimEnd('s'),
50+
includeScope: false,
51+
excludePath: false,
52+
includeLeadingSeparator: true,
53+
includeTrailingSeparator: false,
54+
includeTeamProject: false);
4555
break;
4656
}
4757
default:

CSharp/TfsCmdlets/Controllers/WorkItem/GetWorkItemController.cs

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ partial class GetWorkItemController
1111
[Import]
1212
private IProcessUtil ProcessUtil { get; set; }
1313

14+
[Import]
15+
private INodeUtil NodeUtil { get; set; }
16+
1417
private const int MAX_WORKITEMS = 200;
1518

1619
protected override IEnumerable Run()
@@ -31,10 +34,11 @@ protected override IEnumerable Run()
3134
throw new ArgumentException($"'{Parameters.Get<object>("Project")}' is not a valid project, which is required to execute a saved query. Either supply a valid -Project argument or use Connect-TfsTeamProject prior to invoking this cmdlet.");
3235
}
3336

34-
if (!Deleted && Fields.Length > 0 && Fields[0] != "*")
37+
if (!Deleted && Fields.Length > 0)
3538
{
36-
expand = IncludeLinks ? WorkItemExpand.All : (ShowWindow ? WorkItemExpand.Links : WorkItemExpand.None);
37-
fields = FixWellKnownFields(Fields);
39+
expand = IncludeLinks ? WorkItemExpand.All : (
40+
ShowWindow ? WorkItemExpand.Links : Fields[0] == "*" ? WorkItemExpand.Fields : WorkItemExpand.None);
41+
fields = FixWellKnownFields(Fields).ToList();
3842
}
3943

4044
var ids = new List<int>();
@@ -204,19 +208,19 @@ private IEnumerable<WebApiWorkItem> GetWorkItemsById(IEnumerable<int> ids, DateT
204208
yield break;
205209
}
206210

207-
if(idList.Count <= MAX_WORKITEMS)
211+
if (idList.Count <= MAX_WORKITEMS)
208212
{
209213
var wis = client.GetWorkItemsAsync(idList, fields, asOf, expand, WorkItemErrorPolicy.Fail)
210214
.GetResult("Error getting work items");
211215

212-
foreach(var wi in wis) yield return wi;
213-
216+
foreach (var wi in wis) yield return wi;
217+
214218
yield break;
215219
}
216220

217221
Logger.LogWarn($"Your query resulted in {idList.Count} work items, therefore items must be fetched one at a time. This may take a while. For best performance, write queries that return less than 200 items.");
218222

219-
foreach(var id in idList)
223+
foreach (var id in idList)
220224
{
221225
yield return GetWorkItemById(id, expand, fields, client);
222226
}
@@ -312,33 +316,23 @@ private IEnumerable<string> FixWellKnownFields(IEnumerable<string> fields)
312316

313317
private string BuildSimpleQuery(IEnumerable<string> fields)
314318
{
315-
var sb = new StringBuilder();
316-
317-
sb.Append($"SELECT {string.Join(", ", fields)} FROM WorkItems Where");
318-
319-
var hasCriteria = false;
319+
var criteria = new List<string>();
320+
StringBuilder sb;
320321

321322
foreach (var kvp in SimpleQueryFields)
322323
{
323324
if (!Parameters.HasParameter(kvp.Key)) continue;
324325

326+
sb = new StringBuilder();
325327
var paramValue = Parameters.Get<object>(kvp.Key);
326328

327-
if (hasCriteria)
328-
{
329-
sb.Append(" AND ");
330-
}
331-
else
332-
{
333-
sb.Append(" ");
334-
hasCriteria = true;
335-
}
336-
337329
switch (kvp.Value.Item1)
338330
{
339331
case "Text":
340332
{
341333
var values = ((paramValue as IEnumerable<string>) ?? (IEnumerable<string>)new[] { (string)paramValue }).ToList();
334+
if (values.Count == 0) continue;
335+
342336
sb.Append("(");
343337
for (int i = 0; i < values.Count; i++)
344338
{
@@ -352,6 +346,8 @@ private string BuildSimpleQuery(IEnumerable<string> fields)
352346
case "LongText":
353347
{
354348
var values = ((paramValue as IEnumerable<string>) ?? (IEnumerable<string>)new[] { (string)paramValue }).ToList();
349+
if (values.Count == 0) continue;
350+
355351
sb.Append("(");
356352
for (int i = 0; i < values.Count; i++)
357353
{
@@ -365,6 +361,8 @@ private string BuildSimpleQuery(IEnumerable<string> fields)
365361
case "Number":
366362
{
367363
var values = ((paramValue as IEnumerable<int>) ?? (IEnumerable<int>)new[] { (int)paramValue }).ToList();
364+
if (values.Count == 0) continue;
365+
368366
var op = Ever ? "ever" : "=";
369367
sb.Append("(");
370368
sb.Append(string.Join(" OR ", values.Select(v => $"([{kvp.Value.Item2}] {op} {v})")));
@@ -374,6 +372,8 @@ private string BuildSimpleQuery(IEnumerable<string> fields)
374372
case "Date":
375373
{
376374
var values = ((paramValue as IEnumerable<DateTime>) ?? (IEnumerable<DateTime>)new[] { (DateTime)paramValue }).ToList();
375+
if (values.Count == 0) continue;
376+
377377
var op = Ever ? "ever" : "=";
378378
var format = $"yyyy-MM-dd HH:mm:ss{(TimePrecision ? "HH:mm:ss" : "")}";
379379
sb.Append("(");
@@ -383,15 +383,22 @@ private string BuildSimpleQuery(IEnumerable<string> fields)
383383
}
384384
case "Tree":
385385
{
386-
var values = ((paramValue as IEnumerable<string>) ?? (IEnumerable<string>)new[] { (string)paramValue }).ToList();
386+
var values = ((paramValue as IEnumerable<string>) ?? (IEnumerable<string>)new[] { (string)paramValue })
387+
.Where(v => !string.IsNullOrEmpty(v) && !v.Equals("\\")).ToList();
388+
if (values.Count == 0) continue;
389+
390+
var projectName = Project.Name;
391+
387392
sb.Append("(");
388-
sb.Append(string.Join(" OR ", values.Select(v => $"([{kvp.Value.Item2}] UNDER '{v}')")));
393+
sb.Append(string.Join(" OR ", values.Select(v => $"([{kvp.Value.Item2}] UNDER '{NodeUtil.NormalizeNodePath(v, projectName, includeTeamProject: true, includeLeadingSeparator: false, includeTrailingSeparator: false)}')")));
389394
sb.Append(")");
390395
break;
391396
}
392397
case "Boolean":
393398
{
394399
var values = ((paramValue as IEnumerable<bool>) ?? (IEnumerable<bool>)new[] { (bool)paramValue }).ToList();
400+
if (values.Count == 0) continue;
401+
395402
var op = Ever ? "ever" : "=";
396403
sb.Append("(");
397404
sb.Append(string.Join(" OR ", values.Select(v => $"([{kvp.Value.Item2}] {op} {v})")));
@@ -407,13 +414,20 @@ private string BuildSimpleQuery(IEnumerable<string> fields)
407414
break;
408415
}
409416
}
417+
418+
if (sb.Length > 0)
419+
{
420+
criteria.Add(sb.ToString());
421+
}
410422
}
411423

412-
if (!hasCriteria)
424+
if (criteria.Count == 0)
413425
{
414426
throw new ArgumentException("No filter arguments have been specified. Unable to perform a simple query.");
415427
}
416428

429+
sb = new StringBuilder($"SELECT {string.Join(", ", fields)} FROM WorkItems WHERE {string.Join(" AND ", criteria)}");
430+
417431
if (Has_AsOf)
418432
{
419433
var asOf = AsOf.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ssZ");

Docs/ReleaseNotes/2.6.1.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# TfsCmdlets Release Notes
2+
3+
## Version 2.6.1 (_15/May/2024_)
4+
5+
Ouch! It's been a while since the last release! Sometimes life gets in the way, but I'm back!
6+
7+
This release fixes bugs in `Get-TfsWorkItem`, `Get-TfsArea`, `Get-TfsIteration`, `Invoke-TfsRestApi`, `New-TfsTeam` and `Set-TfsTeam`.
8+
9+
## Fixes
10+
11+
* Fixes [#211](https://github.com/igoravl/TfsCmdlets/issues/211), where `Get-TfsWorkItem` would throw an error when the `-Fields` parameter was "*".
12+
* Fixes a bug in `Invoke-TfsRestApi` where Azure DevOps APIs whose responses were missing the `content-type` header would throw an error.
13+
* Fixes a bug in `Get-TfsArea` and `Get-TfsIteration` where team projects containing Regex-reserved characters (such as parentheses) would throw an error. This bug would indirectly affect `New-TfsTeam` and `Set-TfsTeam` due to their reliance on the same underlying class to handle area and iteration paths when creating/updating teams.
14+
* Fixes a bug in `Get-TfsWorkItem` where the `-AreaPath` and `-IterationPath` parameters would not work when the specified path either started with a backslash or did not contain the team project name.
15+
* Adds the installed module version to the _Azure DevOps Shell_ startup command to prevent loading an older version of the module when the PSModulePath variable contains an older version of the module listed earlier in the search path.

PS/_Tests/RestApi.Invoke-TfsRestApi.Tests.ps1

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ Describe (($MyInvocation.MyCommand.Name -split '\.')[-3]) {
1616
| ForEach-Object { $_.ExtensionName } `
1717
| Sort-Object `
1818
| Select-Object -First 3 `
19-
| Should -Be @('Aex Code Mapper', 'Aex platform', 'Aex user management')
19+
| Should -Be @('AdvancedSecurity', 'Aex Code Mapper', 'Aex platform')
2020
}
2121

2222
It 'Should call multiple alternates hosts in sequence' {
2323
Invoke-TfsRestApi '/_apis/extensionmanagement/installedextensions' -UseHost 'extmgmt.dev.azure.com' -ApiVersion '6.1' `
2424
| ForEach-Object { $_.ExtensionName } `
2525
| Sort-Object `
2626
| Select-Object -First 3 `
27-
| Should -Be @('Aex Code Mapper', 'Aex platform', 'Aex user management')
27+
| Should -Be @('AdvancedSecurity', 'Aex Code Mapper', 'Aex platform')
2828

2929
Invoke-TfsRestApi 'GET https://vsrm.dev.azure.com/{organization}/{project}/_apis/release/definitions?api-version=6.1' -Project $tfsProject `
3030
| ForEach-Object { $_.name } `

0 commit comments

Comments
 (0)