Skip to content

Commit 050c54d

Browse files
dkorpelclaude
andcommitted
changed.d: fetch referenced issues directly so old fixes aren't dropped
Listing all closed issues stopped after 100 pages, and used the oldest commit in the range as the start date, which the merged spec history pushed back to 2015. Issues closed shortly after the previous release fell outside the fetched window, e.g. 14 dmd fixes for 2.113.0. See dlang/dlang.org#4453 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent af8b883 commit 050c54d

1 file changed

Lines changed: 71 additions & 64 deletions

File tree

changed.d

Lines changed: 71 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,34 @@ string escapeParens(string input)
146146
return input.translate(parenToMacro);
147147
}
148148

149+
/**
150+
Get the date of the previous release, which is the start of the revision range
151+
152+
This is more reliable than the date of the oldest commit in the range, because
153+
merging in an unrelated history (like the spec sources) drags in commits that
154+
are years older than the previous release.
155+
*/
156+
Nullable!DateTime getPreviousReleaseDateTime(string revRange)
157+
{
158+
auto parts = revRange.split("..");
159+
if (parts.length < 2 || parts[0].empty)
160+
return Nullable!(DateTime).init;
161+
162+
foreach (repo; ["dmd", "phobos", "dlang.org", "tools", "installer"]
163+
.map!(r => buildPath("..", r)))
164+
{
165+
auto cmd = ["git", "-C", repo, "log", "-1", "--no-patch", "--no-notes"
166+
, "--date=format-local:%Y-%m-%dT%H:%M:%S", "--pretty=%cd"
167+
, parts[0]];
168+
auto p = pipeProcess(cmd, Redirect.stdout);
169+
auto lines = p.stdout.byLineCopy.map!strip.filter!(l => !l.empty).array;
170+
if (wait(p.pid) != 0 || lines.empty)
171+
continue;
172+
return DateTime.fromISOExtString(lines.front).nullable;
173+
}
174+
return Nullable!(DateTime).init;
175+
}
176+
149177
Nullable!DateTime getFirstDateTime(string revRange)
150178
{
151179
DateTime[] all;
@@ -309,8 +337,6 @@ Nullable!int getBugzillaId(string body_)
309337
GithubIssue[][string /*type*/ ][string /*comp*/] getGithubIssuesRest(string revRange,
310338
const DateTime startDate, const DateTime endDate, const string bearer)
311339
{
312-
import std.algorithm.searching : canFind;
313-
314340
GithubIssue[][string][string] ret;
315341
// Keep this list of comps in sync with the switch statement in writeBugzillaChanges
316342
string[2][] comps =
@@ -332,14 +358,10 @@ GithubIssue[][string /*type*/ ][string /*comp*/] getGithubIssuesRest(string revR
332358
continue;
333359

334360
GithubIssue[][string /* type */] tmp;
335-
GithubIssue[] ghi = getGithubIssuesRest("dlang", project, startDate,
336-
endDate, bearer);
361+
GithubIssue[] ghi = getGithubIssuesRest("dlang", project,
362+
issues.githubIssueIds[project], startDate, endDate, bearer);
337363
foreach (jt; ghi)
338364
{
339-
// ignore if closed issue does not have a git log reference
340-
if (!issues.githubIssueIds[project].canFind(jt.id))
341-
continue;
342-
343365
GithubIssue[]* p = jt.type in tmp;
344366
if (p !is null)
345367
{
@@ -356,66 +378,59 @@ GithubIssue[][string /*type*/ ][string /*comp*/] getGithubIssuesRest(string revR
356378
}
357379

358380
/**
359-
Get closed issues of a github project
381+
Get the closed issues of a github project that are referenced in the git log
382+
383+
Only issues closed in the `[startDate, endDate]` window are returned, so that
384+
stale references to unrelated issues with the same number don't end up in the
385+
changelog.
360386
361387
Params:
362388
project = almost always the dlang github project
363389
repo = the name of the repo to get the closed issues for
390+
numbers = the issue numbers referenced by the commits of the release
391+
startDate = issues closed before this date are not part of this release
364392
endDate = the cutoff date for closed issues
365393
bearer = the classic github bearer token
366394
*/
367395
GithubIssue[] getGithubIssuesRest(const string project, const string repo
368-
, const DateTime startDate, const DateTime endDate, const string bearer)
396+
, const int[] numbers, const DateTime startDate, const DateTime endDate
397+
, const string bearer)
369398
{
370-
import std.regex : ctRegex, matchFirst;
371-
372399
GithubIssue[] ret;
373-
// Initial URL for first request
374-
string nextUrl = ("https://api.github.com/repos/%s/%s/issues?per_page=100"
375-
~"&state=closed&since=%s")
376-
.format(project, repo, startDate.toISOExtString() ~ "Z");
377400

378-
foreach (_; 0 .. 100)
379-
{ // 1000 issues per release should be enough
380-
if (nextUrl.empty)
381-
break;
401+
foreach (number; numbers)
402+
{
403+
string url = "https://api.github.com/repos/%s/%s/issues/%d"
404+
.format(project, repo, number);
382405

383-
HTTP http = HTTP(nextUrl);
406+
HTTP http = HTTP(url);
384407
http.addRequestHeader("Accept", "application/vnd.github+json");
385408
http.addRequestHeader("X-GitHub-Api-Version", "2022-11-28");
386409
http.addRequestHeader("Authorization", bearer);
387410

388411
char[] response;
389-
string linkHeader;
390-
try
412+
int statusCode;
413+
http.onReceive = (ubyte[] d)
391414
{
392-
http.onReceive = (ubyte[] d)
393-
{
394-
response ~= cast(char[])d;
395-
return d.length;
396-
};
397-
http.onReceiveHeader = (const(char)[] key, const(char)[] value)
398-
{
399-
if (key == "link")
400-
linkHeader = value.idup;
401-
};
402-
http.perform();
403-
}
404-
catch(Exception e)
415+
response ~= cast(char[])d;
416+
return d.length;
417+
};
418+
http.onReceiveStatusLine = (HTTP.StatusLine line)
405419
{
406-
throw e;
407-
}
420+
statusCode = line.code;
421+
};
422+
http.perform();
423+
424+
// referenced issues may live in another repository
425+
if (statusCode == 404 || statusCode == 410)
426+
continue;
427+
enforce(statusCode == 200, "%s returned status %d:\n%s"
428+
.format(url, statusCode, cast(string)response));
429+
430+
JSONValue it = parseJSON(cast(string)response);
431+
enforce(it.type == JSONType.object, it.toPrettyString()
432+
~ "\nMust be an object");
408433

409-
string s = cast(string)response;
410-
JSONValue j = parseJSON(s);
411-
enforce(j.type == JSONType.array, j.toPrettyString()
412-
~ "\nMust be an array");
413-
JSONValue[] arr = j.arrayNoRef();
414-
if (arr.empty)
415-
{
416-
break;
417-
}
418-
foreach (it; arr)
419434
{
420435
GithubIssue tmp;
421436
// Issues and pull request are both returned by the github api
@@ -455,13 +470,16 @@ GithubIssue[] getGithubIssuesRest(const string project, const string repo
455470
const(JSONValue)* mem = "closed_at" in it;
456471
enforce(mem !is null, it.toPrettyString()
457472
~ "\nmust contain 'closed_at'");
458-
enforce((*mem).type == JSONType.string, (*mem).toPrettyString()
459-
~ "\n'closed_at' must be an string");
473+
// still open, so not part of this release
474+
if ((*mem).type != JSONType.string)
475+
continue;
460476
string d = (*mem).get!string();
461477
d = d.endsWith("Z")
462478
? d[0 .. $ - 1]
463479
: d;
464480
tmp.closedAt = DateTime.fromISOExtString(d);
481+
if (tmp.closedAt < startDate || tmp.closedAt > endDate)
482+
continue;
465483
}
466484
{
467485
const(JSONValue)* mem = "labels" in it;
@@ -502,19 +520,6 @@ GithubIssue[] getGithubIssuesRest(const string project, const string repo
502520
}
503521
ret ~= tmp;
504522
}
505-
506-
// Parse Link header for cursor-based pagination
507-
// Format: <url>; rel="next", <url>; rel="last"
508-
nextUrl = null;
509-
if (!linkHeader.empty)
510-
{
511-
enum linkRe = ctRegex!`<([^>]+)>;\s*rel="next"`;
512-
auto m = matchFirst(linkHeader, linkRe);
513-
if (!m.empty)
514-
{
515-
nextUrl = m[1];
516-
}
517-
}
518523
}
519524
return ret;
520525
}
@@ -813,7 +818,9 @@ Please supply a bugzilla version
813818
, githubClassicTokenFileName));
814819
const string githubToken = readText(githubClassicTokenFileName).strip();
815820

816-
Nullable!(DateTime) firstDate = getFirstDateTime(revRange);
821+
Nullable!(DateTime) firstDate = getPreviousReleaseDateTime(revRange);
822+
if (firstDate.isNull())
823+
firstDate = getFirstDateTime(revRange);
817824
enforce(!firstDate.isNull(), "Couldn't find a date from the revRange");
818825
githubChanges = getGithubIssuesRest(revRange, firstDate.get(), cast(DateTime)currDate
819826
, githubToken);

0 commit comments

Comments
 (0)