Skip to content

Commit 96f4bc6

Browse files
ADO Lens 0.9.3
ADO Lens 0.9.3
2 parents 0110047 + 5b44961 commit 96f4bc6

4 files changed

Lines changed: 40 additions & 8 deletions

File tree

‎CHANGELOG.md‎

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

3+
## 0.9.3 — 2026-10-19
4+
5+
- `ado_trace`: "is the change in this branch / deployment" asks the history of the target around the day the change
6+
was merged (one cheap call) instead of a diff between the commits, which took 20-30 seconds each in a large
7+
repository; the diff stays as the fallback when the date is unknown.
8+
- Code moves forward through the environments of the map (dev -> staging -> master): a change merged into a later
9+
one is not looked for in an earlier one.
10+
- "not yet" leaves out a change whose work is already there through another PR from the same source branch (the same
11+
work merged into each environment by its own PR).
12+
313
## 0.9.2 — 2026-10-19
414

515
Fixes and additions from testing 0.9 on a real project.

‎src/AdoLens/AdoLens.csproj‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
<AssemblyTitle>ADO Lens for Azure DevOps</AssemblyTitle>
2424
<Description>Azure DevOps (work items, boards, wiki, Git and TFVC, pipelines) served to AI assistants over MCP</Description>
2525
<Copyright>Copyright © 2026 WillowDev</Copyright>
26-
<Version>0.9.2</Version>
26+
<Version>0.9.3</Version>
2727
</PropertyGroup>
2828

2929
<ItemGroup>

‎src/AdoLens/Code.cs‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,23 @@ public async Task<CodeChange> PullRequestAsync(string? repoId, int id, bool with
162162
}
163163

164164
/// <summary>Whether <paramref name="commit"/> is in the history of <paramref name="target"/> (a commit or branch).</summary>
165-
public async Task<bool?> ContainsAsync(string repoId, string commit, string target, bool targetIsBranch, CancellationToken ct)
165+
public async Task<bool?> ContainsAsync(string repoId, string commit, string target, bool targetIsBranch, CancellationToken ct, DateTime? committed = null)
166166
{
167+
// With the commit's date: the history of the target around that day holds the commit exactly when the target
168+
// contains it. One cheap call, where a diff between far-apart commits takes seconds in a large repository.
169+
if (committed is { } when)
170+
{
171+
try
172+
{
173+
var near = await client.GetListAsync($"_apis/git/repositories/{repoId}/commits?searchCriteria.itemVersion.version={Uri.EscapeDataString(target)}" +
174+
$"&searchCriteria.itemVersion.versionType={(targetIsBranch ? "branch" : "commit")}" +
175+
$"&searchCriteria.fromDate={Uri.EscapeDataString(when.AddDays(-1).ToString("o", CultureInfo.InvariantCulture))}" +
176+
$"&searchCriteria.toDate={Uri.EscapeDataString(when.AddDays(1).ToString("o", CultureInfo.InvariantCulture))}&searchCriteria.$top=1000", ct);
177+
if (near.Any(c => string.Equals(Json.Str(c, "commitId"), commit, StringComparison.OrdinalIgnoreCase))) return true;
178+
if (near.Count < 1000) return false;
179+
}
180+
catch (AdoApiException) { }
181+
}
167182
try
168183
{
169184
var diff = await client.GetAsync($"_apis/git/repositories/{repoId}/diffs/commits?baseVersion={commit}&baseVersionType=commit" +

‎src/AdoLens/Trace.cs‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -287,15 +287,19 @@ string Label(string? branch, string def, string stage) =>
287287
// with several PRs and many release stages).
288288
var containsCache = new System.Collections.Concurrent.ConcurrentDictionary<(string, string, bool), Task<bool?>>();
289289
using var gate = new SemaphoreSlim(ParallelChecks);
290-
Task<bool?> Contains(string repo, string commit, string target, bool isBranch) =>
291-
containsCache.GetOrAdd((commit, target, isBranch), _ => Gated(() => code.ContainsAsync(repo, commit, target, isBranch, dt)));
290+
Task<bool?> Contains(string repo, string commit, string target, bool isBranch, DateTime? committed) =>
291+
containsCache.GetOrAdd((commit, target, isBranch), _ => Gated(() => code.ContainsAsync(repo, commit, target, isBranch, dt, committed)));
292+
// Code moves forward through the environments (dev -> staging -> master): a change merged into a later one is
293+
// not in an earlier one, so that is not asked.
294+
bool Later(string? changeTarget, string? branch) =>
295+
changeTarget is not null && branch is not null && envOrder.TryGetValue(changeTarget, out var a) && envOrder.TryGetValue(branch, out var b) && a > b;
292296
async Task<bool?> Gated(Func<Task<bool?>> check)
293297
{
294298
await gate.WaitAsync(dt);
295299
try { return await check(); }
296300
finally { gate.Release(); }
297301
}
298-
async Task<(List<CodeChange> In, List<CodeChange> NotYet, bool Known)> GitStatus(string repo, string target, bool isBranch, DateTime? deployedAt)
302+
async Task<(List<CodeChange> In, List<CodeChange> NotYet, bool Known)> GitStatus(string repo, string target, bool isBranch, DateTime? deployedAt, string? targetBranch = null)
299303
{
300304
var git = changes.Select(x => x.Change).Where(x => x.Kind != "changeset" && string.Equals(x.RepoId, repo, StringComparison.OrdinalIgnoreCase)).ToList();
301305
// Only the newest change of each source -> target pair is asked about: the older ones of the pair are in
@@ -308,17 +312,20 @@ string Label(string? branch, string def, string stage) =>
308312
: isBranch && g.Kind == "pr" && string.Equals(g.TargetBranch, target, StringComparison.OrdinalIgnoreCase) ? true
309313
// A deployment that finished before a change was merged cannot contain it: no need to ask.
310314
: deployedAt is { } at && g.Date is { } merged && at < merged ? false
315+
: Later(g.TargetBranch, isBranch ? target : targetBranch) ? false
311316
: !newest.Contains(g) ? (bool?)null
312-
: await Contains(repo, commit, target, isBranch)));
317+
: await Contains(repo, commit, target, isBranch, g.Date)));
313318
// The older changes of a pair follow the answer for its newest one.
314319
for (var i = 0; i < git.Count; i++)
315320
if (answers[i] is null && git[i].LandedCommit is not null
316321
&& git.Select((g, k) => (g, k)).FirstOrDefault(x => newest.Contains(x.g) && x.g.Branch == git[i].Branch && x.g.TargetBranch == git[i].TargetBranch && x.g.Kind == git[i].Kind) is { g: not null } n)
317322
answers[i] = answers[n.k];
318323
var inside = git.Where((g, i) => answers[i] == true).ToList();
319324
var known = answers.Any(a => a is not null);
325+
// Not yet: unless a change from the same source branch is in and no older than a day before it (the same work
326+
// merged into each environment by its own PR).
320327
var notYet = git.Where(g => !inside.Contains(g) && !IsPromotion(g)
321-
&& !inside.Any(i => i.Branch is not null && i.Branch == g.Branch && i.Date >= g.Date)).ToList();
328+
&& !inside.Any(i => i.Branch is not null && i.Branch == g.Branch && i.Date >= g.Date?.AddDays(-1))).ToList();
322329
return (inside, notYet, known);
323330
}
324331
static string Names(IEnumerable<CodeChange> list) =>
@@ -351,7 +358,7 @@ static string Names(IEnumerable<CodeChange> list) =>
351358
{
352359
stageTasks.Add(Task.Run(async () =>
353360
{
354-
var (inside, notYet, _) = await GitStatus(repo, deployedCommit, false, d.When);
361+
var (inside, notYet, _) = await GitStatus(repo, deployedCommit, false, d.When, Code.Short(d.Branch));
355362
if (inside.Count > 0) anyIn = true;
356363
return (string?)(label + (inside.Count > 0
357364
? $"YES - deployed {deployed}: {Names(inside.Where(x => !IsPromotion(x)).DefaultIfEmpty(inside[0]))}" + (notYet.Count > 0 ? $"; not yet: {Names(notYet)}" : "")

0 commit comments

Comments
 (0)