tools/changed.d
anonymous d9ea6a7795 remove wrong P around LIs
BUGSTITLE generates an OL for the LIs.
2016-09-01 22:51:59 +02:00

197 lines
6 KiB
D
Executable file

#!/usr/bin/env rdmd
// Written in the D programming language
/**
Change log generator which fetches the list of bugfixes
from the D Bugzilla between the given dates.
It stores its result in DDoc form to a text file.
Copyright: Dmitry Olshansky 2013.
License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
Authors: Dmitry Olshansky,
Andrej Mitrovic
Example usage:
---
rdmd changed.d --start=2013-01-01 --end=2013-04-01
rdmd changed.d --start=2013-01-01 (end date implicitly set to current date)
---
$(B Note:) The script will cache the results of an invocation, to avoid
re-querying bugzilla when invoked with the same arguments.
Use the --nocache option to override this behavior.
*/
// NOTE: this script requires libcurl to be linked in (usually done by default).
module changed;
import std.net.curl, std.conv, std.exception, std.algorithm, std.csv, std.typecons,
std.stdio, std.datetime, std.array, std.string, std.file, std.format, std.getopt,
std.path;
auto templateRequest =
`https://issues.dlang.org/buglist.cgi?bug_id={buglist}&bug_status=RESOLVED&resolution=FIXED&`~
`ctype=csv&columnlist=component,bug_severity,short_desc`;
auto generateRequest(Range)(string templ, Range issues)
{
auto buglist = format("%(%d,%)", issues);
return templateRequest.replace("{buglist}", buglist);
}
auto dateFromStr(string sdate)
{
int year, month, day;
formattedRead(sdate, "%s-%s-%s", &year, &month, &day);
return Date(year, month, day);
}
struct Entry
{
int id;
string summary;
}
string[dchar] parenToMacro;
shared static this()
{
parenToMacro = ['(' : "$(LPAREN)", ')' : "$(RPAREN)"];
}
/** Replace '(' and ')' with macros to avoid closing down macros by accident. */
string escapeParens(string input)
{
return input.translate(parenToMacro);
}
/** Get a list of all bugzilla issues mentioned in revRange */
auto getIssues(string revRange)
{
import std.process : pipeProcess, Redirect, wait;
import std.regex : ctRegex, match, splitter;
// see https://github.com/github/github-services/blob/2e886f407696261bd5adfc99b16d36d5e7b50241/lib/services/bugzilla.rb#L155
enum closedRE = ctRegex!(`((close|fix|address)e?(s|d)? )?(ticket|bug|tracker item|issue)s?:? *([\d ,\+&#and]+)`, "i");
auto issues = appender!(int[]);
foreach (repo; ["dmd", "druntime", "phobos", "dlang.org", "tools", "installer"]
.map!(r => buildPath("..", r)))
{
auto cmd = ["git", "-C", repo, "fetch", "upstream", "--tags"];
auto p = pipeProcess(cmd, Redirect.stdout);
enforce(wait(p.pid) == 0, "Failed to execute '%(%s %)'.".format(cmd));
cmd = ["git", "-C", repo, "log", revRange];
p = pipeProcess(cmd, Redirect.stdout);
scope(exit) enforce(wait(p.pid) == 0, "Failed to execute '%(%s %)'.".format(cmd));
foreach (line; p.stdout.byLine())
{
if (auto m = match(line, closedRE))
{
if (!m.captures[1].length) continue;
m.captures[5]
.splitter(ctRegex!`[^\d]+`)
.filter!(b => b.length)
.map!(to!int)
.copy(issues);
}
}
}
return issues.data.sort().release.uniq;
}
/** Generate and return the change log as a string. */
string getChangeLog(string revRange)
{
auto req = generateRequest(templateRequest, getIssues(revRange));
debug stderr.writeln(req); // write text
auto data = req.get;
// component (e.g. DMD) -> bug type (e.g. regression) -> list of bug entries
Entry[][string][string] entries;
immutable bugtypes = ["regressions", "bugs", "enhancements"];
immutable components = ["DMD Compiler", "Phobos", "Druntime", "dlang.org", "Optlink", "Tools", "Installer"];
foreach (fields; csvReader!(Tuple!(int, string, string, string))(data, null))
{
string comp = fields[1].toLower;
switch (comp)
{
case "dlang.org": comp = "dlang.org"; break;
case "dmd": comp = "DMD Compiler"; break;
case "druntime": comp = "Druntime"; break;
case "installer": comp = "Installer"; break;
case "phobos": comp = "Phobos"; break;
case "tools": comp = "Tools"; break;
default: assert(0, comp);
}
string type = fields[2].toLower;
switch (type)
{
case "regression":
type = "regressions";
break;
case "blocker", "critical", "major", "normal", "minor", "trivial":
type = "bugs";
break;
case "enhancement":
type = "enhancements";
break;
default: assert(0, type);
}
entries[comp][type] ~= Entry(fields[0], fields[3].idup);
}
Appender!string result;
result ~= "$(BUGSTITLE Language Changes,\n";
result ~= "-- Insert major language changes here --\n)\n\n";
result ~= "$(BUGSTITLE Library Changes,\n";
result ~= "-- Insert major library changes here --\n)\n\n";
result ~= "$(BR)$(BIG List of all bug fixes and enhancements:)\n\n";
foreach (component; components)
if (auto comp = component in entries)
{
foreach (bugtype; bugtypes)
if (auto bugs = bugtype in *comp)
{
result ~= format("$(BUGSTITLE %s %s,\n\n", component, bugtype);
foreach (bug; sort!"a.id < b.id"(*bugs))
{
result ~= format("$(LI $(BUGZILLA %s): %s)\n",
bug.id, bug.summary.escapeParens());
}
result ~= ")\n";
}
}
return result.data;
}
int main(string[] args)
{
if (args.length != 2)
{
stderr.writeln("Usage: ./changed <revision range>, e.g. ./changed v2.067.1..upstream/stable");
return 1;
}
string logPath = "./changelog.txt".absolutePath.buildNormalizedPath;
std.file.write(logPath, getChangeLog(args[1]));
writefln("Change log generated to: '%s'", logPath);
return 0;
}