#!/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 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; }