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