Skip to content

Commit 6fe105e

Browse files
committed
Add authors script to generate authors from repo
1 parent 4c6b984 commit 6fe105e

File tree

1 file changed

+182
-0
lines changed

1 file changed

+182
-0
lines changed

scripts/authors.ts

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import fs = require('fs');
2+
import path = require('path');
3+
import child_process = require("child_process");
4+
5+
type Author = {
6+
displayNames: string[];
7+
preferedName?: string;
8+
emails: string[];
9+
};
10+
11+
type AuthorMap = { [s: string]: Author };
12+
13+
type Command = {
14+
(...arg: string[]): void;
15+
description?: string;
16+
};
17+
18+
const mailMapPath = path.resolve("../.mailmap");
19+
const authorsPath = path.resolve("../AUTHORS.md");
20+
21+
function getKnownAuthors(): Author[] {
22+
const segmentRegExp = /\s?([^<]+)\s+<([^>]+)>/g;
23+
const preferedNameRegeExp = /\s?#\s?([^#]+)$/;
24+
const knownAuthors: Author[] = [];
25+
26+
if (!fs.existsSync(mailMapPath)) {
27+
throw new Error(`Could not load known users form .mailmap file at: ${mailMapPath}`);
28+
}
29+
30+
const mailMap = fs.readFileSync(mailMapPath).toString();
31+
32+
for (const line of mailMap.split("\r\n")) {
33+
const author: Author = { displayNames: [], emails: [] };
34+
let match: RegExpMatchArray | null;
35+
36+
while (match = segmentRegExp.exec(line)) {
37+
author.displayNames.push(match[1]);
38+
author.emails.push(match[2]);
39+
}
40+
if (match = preferedNameRegeExp.exec(line)) {
41+
author.preferedName = match[1];
42+
}
43+
if (!author.emails) continue;
44+
knownAuthors.push(author);
45+
if (line.indexOf("#") > 0 && !author.preferedName) {
46+
throw new Error("Could not match prefered name for: " + line);
47+
}
48+
// console.log("===> line: " + line);
49+
// console.log(JSON.stringify(author, undefined, 2));
50+
}
51+
return knownAuthors;
52+
}
53+
54+
function getAuthorName(author: Author) {
55+
return author.preferedName || author.displayNames[0];
56+
}
57+
58+
function getKnownAuthorMaps() {
59+
const knownAuthors = getKnownAuthors();
60+
const authorsByName: AuthorMap = {};
61+
const authorsByEmail: AuthorMap = {};
62+
knownAuthors.forEach(author => {
63+
author.displayNames.forEach(n => authorsByName[n] = author);
64+
author.emails.forEach(e => authorsByEmail[e.toLocaleLowerCase()] = author);
65+
});
66+
return {
67+
knownAuthors,
68+
authorsByName,
69+
authorsByEmail
70+
};
71+
}
72+
73+
function deduplicate<T>(array: T[]): T[] {
74+
let result: T[] = []
75+
if (array) {
76+
for (const item of array) {
77+
if (result.indexOf(item) < 0) {
78+
result.push(item);
79+
}
80+
}
81+
}
82+
return result;
83+
}
84+
85+
function log(s: string) {
86+
console.log(` ${s}`);
87+
}
88+
89+
function sortAuthors(a: string, b: string) {
90+
if (a.charAt(0) === "@") a = a.substr(1);
91+
if (b.charAt(0) === "@") b = b.substr(1);
92+
if (a.toLocaleLowerCase() < b.toLocaleLowerCase()) {
93+
return -1;
94+
}
95+
else {
96+
return 1;
97+
}
98+
}
99+
100+
namespace Commands {
101+
export const writeAuthors: Command = function () {
102+
const output = deduplicate(getKnownAuthors().map(getAuthorName).filter(a => !!a)).sort(sortAuthors).join("\r\n* ");
103+
fs.writeFileSync(authorsPath, "TypeScript is authored by:\r\n* " + output);
104+
};
105+
writeAuthors.description = "Write known authors to AUTHORS.md file.";
106+
107+
export const listKnownAuthors: Command = function () {
108+
deduplicate(getKnownAuthors().map(getAuthorName)).filter(a => !!a).sort(sortAuthors).forEach(log);
109+
};
110+
listKnownAuthors.description = "List known authors as listed in .mailmap file.";
111+
112+
export const listAuthors: Command = function (spec = "") {
113+
const cmd = "git shortlog -se " + spec;
114+
console.log(cmd);
115+
const outputRegExp = /\d+\s+([^<]+)<([^>]+)>/;
116+
const tty = process.platform === 'win32' ? 'CON' : '/dev/tty';
117+
const authors: { name: string, email: string, knownAuthor?: Author }[] = [];
118+
child_process.exec(`${cmd} < ${tty}`, { cwd: path.resolve("../") }, function (error, stdout, stderr) {
119+
if (error) {
120+
console.log(stderr.toString());
121+
}
122+
else {
123+
const output = stdout.toString();
124+
const lines = output.split("\n");
125+
lines.forEach(line => {
126+
if (line) {
127+
let match: RegExpExecArray | null;
128+
if (match = outputRegExp.exec(line)) {
129+
authors.push({ name: match[1], email: match[2] });
130+
}
131+
else {
132+
throw new Error("Could not parse output: " + line);
133+
}
134+
}
135+
});
136+
137+
const maps = getKnownAuthorMaps();
138+
139+
const lookupAuthor = function ({name, email}: { name: string, email: string }) {
140+
return maps.authorsByEmail[email.toLocaleLowerCase()] || maps.authorsByName[name];
141+
};
142+
143+
const knownAuthors = authors
144+
.map(lookupAuthor)
145+
.filter(a => !!a)
146+
.map(getAuthorName);
147+
const unknownAuthors = authors
148+
.filter(a => !lookupAuthor(a))
149+
.map(a => `${a.name} <${a.email}>`);
150+
151+
if (knownAuthors.length) {
152+
console.log("\r\n");
153+
console.log("Found known authors: ");
154+
console.log("=====================");
155+
deduplicate(knownAuthors).sort(sortAuthors).forEach(log);
156+
}
157+
158+
if (unknownAuthors.length) {
159+
console.log("\r\n");
160+
console.log("Found unknown authors: ");
161+
console.log("=====================");
162+
deduplicate(unknownAuthors).sort(sortAuthors).forEach(log);
163+
}
164+
}
165+
});
166+
};
167+
listAuthors.description = "List known and unknown authors for a given spec";
168+
}
169+
170+
var args = process.argv.slice(2);
171+
if (args.length < 1) {
172+
console.log('Usage: node authors.js [command]');
173+
console.log('List of commands: ');
174+
Object.keys(Commands).forEach(k => console.log(` ${k}: ${(Commands as any)[k]['description']}`));
175+
} else {
176+
var cmd: Function = (Commands as any)[args[0]];
177+
if (cmd === undefined) {
178+
console.log('Unknown command ' + args[1]);
179+
} else {
180+
cmd.apply(undefined, args.slice(1));
181+
}
182+
}

0 commit comments

Comments
 (0)