tools/contributors.d

159 lines
4.8 KiB
D
Executable file

#!/usr/bin/env rdmd
/**
Query contributors between two D releases.
Copyright: D Language Foundation 2017.
License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
Example usage:
---
./contributors.d "v2.074.0..v2.075.0"
---
Author: Sebastian Wilzbach
*/
import std.array;
import std.algorithm;
import std.conv;
import std.exception;
import std.file;
import std.format;
import std.process;
import std.path;
import std.range;
import std.stdio;
import std.string;
import std.typecons;
/// Name <my@email.com>
struct GitAuthor
{
string name, email;
string toString()
{
return "%s <%s>".format(name, email);
}
}
/// Options for finding authors
struct FindConfig
{
bool refreshTags; /// will query github.com for new tags
bool noMerges; // will ignore merge commits
bool showAllContributors; // will ignore the revRange and show all contributors
string cwd; // working directory (should be tools)
string mailmapFile; // location to the .mailmap file
}
/**
Search all git commit messages within revRange of all D repositories
Returns: Array that maps each git `Author: ...` line to a GitAuthor
*/
auto findAuthors(string revRange, FindConfig config)
{
Appender!(GitAuthor[]) authors;
int commits;
auto repos = ["dmd", "phobos", "dlang.org", "tools", "installer"];
if (config.showAllContributors)
repos ~= ["dub", "dub-registry", "dconf.org"];
foreach (repo; repos.map!(r => buildPath(config.cwd, "..", r)))
{
if (!repo.exists)
{
stderr.writefln("Warning: %s doesn't exist. " ~
"Consider running: git clone https://github.com/dlang/%s ../%2$s",
repo, repo.baseName);
continue;
}
if (config.refreshTags)
{
auto cmd = ["git", "-C", repo, "fetch", "--tags", "https://github.com/dlang/" ~ repo.baseName,
"+refs/heads/*:refs/remotes/upstream/*"];
auto p = pipeProcess(cmd, Redirect.stdout);
enforce(wait(p.pid) == 0, "Failed to execute '%(%s %)'.".format(cmd));
}
auto cmd = ["git", "-c", "mailmap.file=%s".format(config.mailmapFile), "-C", repo, "log", "--use-mailmap", "--pretty=format:%aN|%aE"];
if (!config.showAllContributors)
cmd ~= revRange;
if (config.noMerges)
cmd ~= "--no-merges";
auto p = pipeProcess(cmd, Redirect.stdout);
scope(exit) enforce(wait(p.pid) == 0, "Failed to execute '%(%s %)'.".format(cmd));
authors ~= p.stdout
.byLineCopy
.tee!(_ => commits++)
.map!((line){
auto ps = line.splitter("|");
return GitAuthor(ps.front, ps.dropOne.front);
})
.filter!(a => a.name != "The Dlang Bot");
}
if (!config.showAllContributors)
stderr.writefln("Looked at %d commits in %s", commits, revRange);
else
stderr.writefln("Looked at %d commits", commits);
return authors.data;
}
/// Sorts the authors and filters for duplicates
auto reduceAuthors(GitAuthors)(GitAuthors authors)
{
import std.uni : sicmp;
return authors
.sort!((a, b) => sicmp(a.name, b.name) < 0)
.uniq!((a, b) => a.name == b.name);
}
version(Contributors_Lib) {} else
int main(string[] args)
{
import std.getopt;
string revRange;
FindConfig config = {
cwd: __FILE_FULL_PATH__.dirName.asNormalizedPath.to!string,
};
config.mailmapFile = config.cwd.buildPath(".mailmap");
enum PrintMode { name, markdown, ddoc, csv, git}
PrintMode printMode;
auto helpInformation = getopt(
args,
std.getopt.config.passThrough,
"f|format", "Result format (name, markdown, ddoc, csv, git)", &printMode,
"a|all", "Show all contributors", &config.showAllContributors,
"refresh-tags", "Refresh tags", &config.refreshTags,
"no-merges", "Ignore merge commits", &config.noMerges,
);
if (helpInformation.helpWanted || (args.length < 2 && !config.showAllContributors))
{
`D contributors extractor.
./contributors.d "v2.075.0..v2.076.0"`.defaultGetoptPrinter(helpInformation.options);
return 1;
}
revRange = args.length > 1 ? args[1] : null;
revRange.findAuthors(config)
.reduceAuthors
.each!((a){
with(PrintMode)
final switch (printMode)
{
case name: a.name.writeln; break;
case markdown: writefln("- %s", a.name); break;
case ddoc: writefln("$(D_CONTRIBUTOR %s)", a.name); break;
case csv: writefln("%s, %s", a.name, a.email); break;
case git: writefln("%s <%s>", a.name, a.email); break;
}
});
return 0;
}