Skip to content

Commit a266cb3

Browse files
ADO Lens 0.11.2
- Fixed: finding the environments (settings window, `adolens setup`, `ado_settings`, the SETUP hint of `ado_trace`) looked only in the project of the work items, not in the project with the code set with `--code-project`. It now looks in both: the repositories, branches and release stages of the code project are offered too. The settings window says which project the code is in.
1 parent b3dc1fb commit a266cb3

5 files changed

Lines changed: 61 additions & 19 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## 0.11.2 — 2026-10-27
4+
5+
- Fixed: finding the environments (settings window, `adolens setup`, `ado_settings`, the SETUP hint of `ado_trace`)
6+
looked only in the project of the work items, not in the project with the code set with `--code-project`. It now
7+
looks in both: the repositories, branches and release stages of the code project are offered too. The settings
8+
window says which project the code is in.
9+
310
## 0.11.1 — 2026-10-27
411

512
- Fixed: the program did not start ("Failed to determine default extraction location") when TEMP pointed to a folder

‎src/AdoLens/AdoLens.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<AssemblyTitle>ADO Lens for Azure DevOps</AssemblyTitle>
2626
<Description>Azure DevOps (work items, boards, wiki, Git and TFVC, pipelines) served to AI assistants over MCP</Description>
2727
<Copyright>Copyright © 2026 WillowDev</Copyright>
28-
<Version>0.11.1</Version>
28+
<Version>0.11.2</Version>
2929
</PropertyGroup>
3030

3131
<ItemGroup>

‎src/AdoLens/EnvironmentFlow.cs‎

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,57 @@ public static class EnvironmentFlow
1616
return (new Dictionary<string, string>(), "none");
1717
}
1818

19+
/// <summary>
20+
/// Release stages and long-lived branches of the project with the code (the connection's code project, when the
21+
/// code lives elsewhere than the work items) and of <paramref name="project"/> itself.
22+
/// </summary>
1923
public static async Task<List<(string Key, string? Guess)>> DetectAsync(Session session, string project, CancellationToken ct)
2024
{
25+
var projects = new[] { session.Config.CodeProject, project }.Where(p => !string.IsNullOrWhiteSpace(p)).Select(p => p!)
26+
.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
2127
var keys = new List<string>();
22-
foreach (var d in await session.Code.DeploymentsAsync(project, ct))
23-
{
24-
if (d.When is not { } when || when < DateTime.UtcNow.AddDays(-183)) continue;
25-
keys.Add(d.Branch?.StartsWith("$/", StringComparison.Ordinal) == true ? $"{d.Definition}/{d.Stage}" : Code.Short(d.Branch) ?? $"{d.Definition}/{d.Stage}");
26-
}
2728
// Long-lived branches of the repositories in use (a commit in the last half year); feature and work branches are
2829
// filtered by ProposeFlow. With several repositories each branch is named with its repository ("D365FO:staging"),
2930
// since the same branch name can mean different environments - or none - in different repositories.
30-
try
31+
var active = new List<(string Id, string Name)>();
32+
foreach (var p in projects)
3133
{
32-
var repos = await session.Client.GetListAsync($"{AdoClient.Scope(project)}_apis/git/repositories", ct);
33-
var active = new List<(string Id, string Name)>();
34-
foreach (var repo in repos.Where(r => !Json.Bool(r, "isDisabled") && r.TryGetProperty("size", out var s) && s.GetInt64() > 0).Take(30))
34+
try
3535
{
36-
var id = Json.Str(repo, "id") ?? "";
37-
var last = await session.Client.GetListAsync($"_apis/git/repositories/{id}/commits?searchCriteria.$top=1", ct);
38-
if (last.Count > 0 && DateTime.TryParse(Json.Str(Json.Obj(last[0], "committer"), "date"), System.Globalization.CultureInfo.InvariantCulture,
39-
System.Globalization.DateTimeStyles.AdjustToUniversal, out var when) && when > DateTime.UtcNow.AddDays(-183))
40-
active.Add((id, Json.Str(repo, "name") ?? id));
36+
var repos = await session.Client.GetListAsync($"{AdoClient.Scope(p)}_apis/git/repositories", ct);
37+
foreach (var repo in repos.Where(r => !Json.Bool(r, "isDisabled") && r.TryGetProperty("size", out var s) && s.GetInt64() > 0).Take(30))
38+
{
39+
var id = Json.Str(repo, "id") ?? "";
40+
var last = await session.Client.GetListAsync($"_apis/git/repositories/{id}/commits?searchCriteria.$top=1", ct);
41+
if (last.Count > 0 && DateTime.TryParse(Json.Str(Json.Obj(last[0], "committer"), "date"), System.Globalization.CultureInfo.InvariantCulture,
42+
System.Globalization.DateTimeStyles.AdjustToUniversal, out var when) && when > DateTime.UtcNow.AddDays(-183))
43+
active.Add((id, Json.Str(repo, "name") ?? id));
44+
}
4145
}
42-
foreach (var (id, name) in active)
46+
catch (AdoApiException) { } // no Git, or no Code (Read) scope
47+
}
48+
var several = active.Count > 1;
49+
foreach (var (id, name) in active)
50+
{
51+
try
4352
{
4453
var refs = await session.Client.GetListAsync($"_apis/git/repositories/{id}/refs?filter=heads/&$top=200", ct);
4554
keys.AddRange(refs.Select(r => Code.Short(Json.Str(r, "name")) ?? "").Where(n => n.Length > 0 && !n.Contains('/'))
46-
.Select(n => active.Count > 1 ? $"{name}:{n}" : n));
55+
.Select(n => several ? $"{name}:{n}" : n));
4756
}
57+
catch (AdoApiException) { }
4858
}
49-
catch (AdoApiException) { } // no Git, or no Code (Read) scope
59+
// Release stages deployed in the last half year; a Git stage is named by its branch (with its repository when
60+
// there are several), a TFVC one as "Definition/Stage".
61+
foreach (var p in projects)
62+
foreach (var d in await session.Code.DeploymentsAsync(p, ct))
63+
{
64+
if (d.When is not { } when || when < DateTime.UtcNow.AddDays(-183)) continue;
65+
var branch = Code.Short(d.Branch);
66+
var repo = active.FirstOrDefault(a => string.Equals(a.Id, d.Repository, StringComparison.OrdinalIgnoreCase)).Name;
67+
keys.Add(d.Branch?.StartsWith("$/", StringComparison.Ordinal) == true || branch is null ? $"{d.Definition}/{d.Stage}"
68+
: several && repo is not null ? $"{repo}:{branch}" : branch);
69+
}
5070
return TraceQueries.ProposeFlow(keys.Distinct(StringComparer.OrdinalIgnoreCase));
5171
}
5272
}

‎src/AdoLens/SettingsWindow.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,8 @@ async Task LoadEnvironmentsAsync(bool detect)
337337
if (detect || map.Count == 0)
338338
foreach (var (key, guess) in await EnvironmentFlow.DetectAsync(session, project, default))
339339
if (!map.ContainsKey(key)) { AddRow(key, guess ?? ""); found++; }
340-
envInfo.Text = $"{project}: " + (map.Count > 0 ? Ui.T($"in use: {source}", $"w użyciu: {source}") : Ui.T("no map yet", "brak mapy"))
340+
var code = session.Config.CodeProject is { } cp && !cp.Equals(project, StringComparison.OrdinalIgnoreCase) ? Ui.T($" (code in {cp})", $" (kod w {cp})") : "";
341+
envInfo.Text = $"{project}{code}: " + (map.Count > 0 ? Ui.T($"in use: {source}", $"w użyciu: {source}") : Ui.T("no map yet", "brak mapy"))
341342
+ (found > 0 ? Ui.T($"; {found} found (check the names)", $"; znalezione: {found} (sprawdź nazwy)") : "");
342343
}
343344
catch (Exception ex) when (ex is InvalidOperationException or AdoApiException or HttpRequestException)

‎tests/AdoLens.Tests/SetupFlowTests.cs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ public TeamSettingsTests()
7373
{"value":[{"releaseDefinition":{"name":"Shop"},"releaseEnvironment":{"name":"lcs"},"completedOn":"2099-01-01T00:00:00Z",
7474
"release":{"artifacts":[{"definitionReference":{"branch":{"id":"refs/heads/staging"},"sourceVersion":{"id":"x"}}}]}}]}
7575
""")
76+
.On("/contoso/ERP/_apis/git/repositories", """{"value":[{"id":"r2","name":"erp","size":100}]}""")
77+
.On("/contoso/_apis/git/repositories/r2/commits", () => "{\"value\":[{\"commitId\":\"c2\",\"committer\":{\"date\":\"" + DateTime.UtcNow.AddDays(-1).ToString("o") + "\"}}]}")
78+
.On("/contoso/_apis/git/repositories/r2/refs", """{"value":[{"name":"refs/heads/staging"},{"name":"refs/heads/123456_LoginForm_AB"}]}""")
7679
.On("/contoso/Demo/_apis/git/repositories", """{"value":[{"id":"r1","name":"portal","size":100}]}""")
7780
.On("/contoso/_apis/git/repositories/r1/commits", () => "{\"value\":[{\"commitId\":\"c1\",\"committer\":{\"date\":\"" + DateTime.UtcNow.AddDays(-3).ToString("o") + "\"}}]}")
7881
.On("/contoso/_apis/git/repositories/r1/refs", """{"value":[{"name":"refs/heads/dev"},{"name":"refs/heads/master"},{"name":"refs/heads/feature/x"}]}""");
@@ -87,6 +90,17 @@ public void Dispose()
8790
if (Directory.Exists(dir)) Directory.Delete(dir, recursive: true);
8891
}
8992

93+
[Fact]
94+
public async Task With_the_code_in_another_project_its_branches_are_found_too()
95+
{
96+
settings.Connections["contoso"].CodeProject = "ERP";
97+
var keys = (await EnvironmentFlow.DetectAsync(lens.For(null), "Demo", default)).Select(d => d.Key).ToList();
98+
// Two repositories, one per project: each branch with its repository; work branches left out.
99+
Assert.Contains("erp:staging", keys);
100+
Assert.Contains("portal:dev", keys);
101+
Assert.DoesNotContain(keys, k => k.Contains("LoginForm"));
102+
}
103+
90104
[Fact]
91105
public void The_wiki_table_round_trips_in_order()
92106
{

0 commit comments

Comments
 (0)