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