main.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. /*
  2. * Copyright (C) 2024 Puter Technologies Inc.
  3. *
  4. * This file is part of Puter.
  5. *
  6. * Puter is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published
  8. * by the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. const levenshtein = require('js-levenshtein');
  20. const DiffMatchPatch = require('diff-match-patch');
  21. const enq = require('enquirer');
  22. const dmp = new DiffMatchPatch();
  23. const dedent = require('dedent');
  24. const { walk, EXCLUDE_LISTS } = require('file-walker');
  25. const { CommentParser } = require('../comment-parser/main');
  26. const fs = require('fs');
  27. const path_ = require('path');
  28. const CompareFn = ({ header1, header2, distance_only = false }) => {
  29. // Calculate Levenshtein distance
  30. const distance = levenshtein(header1, header2);
  31. // console.log(`Levenshtein distance: ${distance}`);
  32. if ( distance_only ) return { distance };
  33. // Generate diffs using diff-match-patch
  34. const diffs = dmp.diff_main(header1, header2);
  35. dmp.diff_cleanupSemantic(diffs);
  36. let term_diff = '';
  37. // Manually format diffs for terminal display
  38. diffs.forEach(([type, text]) => {
  39. switch (type) {
  40. case DiffMatchPatch.DIFF_INSERT:
  41. term_diff += `\x1b[32m${text}\x1b[0m`; // Green for insertions
  42. break;
  43. case DiffMatchPatch.DIFF_DELETE:
  44. term_diff += `\x1b[31m${text}\x1b[0m`; // Red for deletions
  45. break;
  46. case DiffMatchPatch.DIFF_EQUAL:
  47. term_diff += text; // No color for equal parts
  48. break;
  49. }
  50. });
  51. return {
  52. distance,
  53. term_diff,
  54. };
  55. }
  56. const LicenseChecker = ({
  57. comment_parser,
  58. desired_header,
  59. }) => {
  60. const supports = ({ filename }) => {
  61. return comment_parser.supports({ filename });
  62. };
  63. const compare = async ({ filename, source }) => {
  64. const headers = await comment_parser.extract_top_comments(
  65. { filename, source });
  66. const headers_lines = headers.map(h => h.lines);
  67. if ( headers.length < 1 ) {
  68. return {
  69. has_header: false,
  70. };
  71. }
  72. // console.log('headers', headers);
  73. let top = 0;
  74. let bottom = 0;
  75. let current_distance = Number.MAX_SAFE_INTEGER;
  76. // "wah"
  77. for ( let i=1 ; i <= headers.length ; i++ ) {
  78. const combined = headers_lines.slice(top, i).flat();
  79. const combined_txt = combined.join('\n');
  80. const { distance } =
  81. CompareFn({
  82. header1: desired_header,
  83. header2: combined_txt,
  84. distance_only: true,
  85. });
  86. if ( distance < current_distance ) {
  87. current_distance = distance;
  88. bottom = i;
  89. } else {
  90. break;
  91. }
  92. }
  93. // "woop"
  94. for ( let i=1 ; i < headers.length ; i++ ) {
  95. const combined = headers_lines.slice(i, bottom).flat();
  96. const combined_txt = combined.join('\n');
  97. const { distance } =
  98. CompareFn({
  99. header1: desired_header,
  100. header2: combined_txt,
  101. distance_only: true,
  102. });
  103. if ( distance < current_distance ) {
  104. current_distance = distance;
  105. top = i;
  106. } else {
  107. break;
  108. }
  109. }
  110. // console.log('headers', headers);
  111. const combined = headers_lines.slice(top, bottom).flat();
  112. const combined_txt = combined.join('\n');
  113. const diff_info = CompareFn({
  114. header1: desired_header,
  115. header2: combined_txt,
  116. })
  117. if ( diff_info.distance > 0.7*desired_header.length ) {
  118. return {
  119. has_header: false,
  120. };
  121. }
  122. diff_info.range = [
  123. headers[top].range[0],
  124. headers[bottom-1].range[1],
  125. ];
  126. diff_info.has_header = true;
  127. return diff_info;
  128. };
  129. return {
  130. compare,
  131. supports,
  132. };
  133. };
  134. const license_check_test = async ({ options }) => {
  135. const comment_parser = CommentParser();
  136. const license_checker = LicenseChecker({
  137. comment_parser,
  138. desired_header: fs.readFileSync(
  139. path_.join(__dirname, '../../doc/license_header.txt'),
  140. 'utf-8',
  141. ),
  142. });
  143. const walk_iterator = walk({
  144. excludes: EXCLUDE_LISTS.NOT_AGPL,
  145. }, path_.join(__dirname, '../..'));
  146. for await ( const value of walk_iterator ) {
  147. if ( value.is_dir ) continue;
  148. if ( value.name !== 'dev-console-ui-utils.js' ) continue;
  149. console.log(value.path);
  150. const source = fs.readFileSync(value.path, 'utf-8');
  151. const diff_info = await license_checker.compare({
  152. filename: value.name,
  153. source,
  154. })
  155. if ( diff_info ) {
  156. process.stdout.write('\x1B[36;1m=======\x1B[0m\n');
  157. process.stdout.write(diff_info.term_diff);
  158. process.stdout.write('\n\x1B[36;1m=======\x1B[0m\n');
  159. // console.log('headers', headers);
  160. } else {
  161. console.log('NO COMMENT');
  162. }
  163. console.log('RANGE', diff_info.range)
  164. const new_comment = comment_parser.output_comment({
  165. filename: value.name,
  166. style: 'block',
  167. text: 'some text\nto display'
  168. });
  169. console.log('NEW COMMENT?', new_comment);
  170. }
  171. };
  172. const cmd_check_fn = async () => {
  173. const comment_parser = CommentParser();
  174. const license_checker = LicenseChecker({
  175. comment_parser,
  176. desired_header: fs.readFileSync(
  177. path_.join(__dirname, '../../doc/license_header.txt'),
  178. 'utf-8',
  179. ),
  180. });
  181. const counts = {
  182. ok: 0,
  183. missing: 0,
  184. conflict: 0,
  185. error: 0,
  186. unsupported: 0,
  187. };
  188. const walk_iterator = walk({
  189. excludes: EXCLUDE_LISTS.NOT_AGPL,
  190. }, path_.join(__dirname, '../..'));
  191. for await ( const value of walk_iterator ) {
  192. if ( value.is_dir ) continue;
  193. process.stdout.write(value.path + ' ... ');
  194. if ( ! license_checker.supports({ filename: value.name }) ) {
  195. process.stdout.write(`\x1B[37;1mUNSUPPORTED\x1B[0m\n`);
  196. counts.unsupported++;
  197. continue;
  198. }
  199. const source = fs.readFileSync(value.path, 'utf-8');
  200. const diff_info = await license_checker.compare({
  201. filename: value.name,
  202. source,
  203. })
  204. if ( ! diff_info ) {
  205. counts.error++;
  206. continue;
  207. }
  208. if ( ! diff_info.has_header ) {
  209. counts.missing++;
  210. process.stdout.write(`\x1B[33;1mMISSING\x1B[0m\n`);
  211. continue;
  212. }
  213. if ( diff_info ) {
  214. if ( diff_info.distance !== 0 ) {
  215. counts.conflict++;
  216. process.stdout.write(`\x1B[31;1mCONFLICT\x1B[0m\n`);
  217. } else {
  218. counts.ok++;
  219. process.stdout.write(`\x1B[32;1mOK\x1B[0m\n`);
  220. }
  221. } else {
  222. console.log('NO COMMENT');
  223. }
  224. }
  225. const { Table } = require('console-table-printer');
  226. const t = new Table({
  227. columns: [
  228. {
  229. title: 'License Header',
  230. name: 'situation', alignment: 'left', color: 'white_bold' },
  231. {
  232. title: 'Number of Files',
  233. name: 'count', alignment: 'right' },
  234. ],
  235. colorMap: {
  236. green: '\x1B[32;1m',
  237. yellow: '\x1B[33;1m',
  238. red: '\x1B[31;1m',
  239. }
  240. });
  241. console.log('');
  242. if ( counts.error > 0 ) {
  243. console.log(`\x1B[31;1mTHERE WERE SOME ERRORS!\x1B[0m`);
  244. console.log('check the log above for the stack trace');
  245. console.log('');
  246. t.addRow({ situation: 'error', count: counts.error },
  247. { color: 'red' });
  248. }
  249. console.log(dedent(`
  250. \x1B[31;1mAny text below is mostly lies!\x1B[0m
  251. This tool is still being developed and most of what's
  252. described is "the plan" rather than a thing that will
  253. actually happen.
  254. \x1B[31;1m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\x1B[0m
  255. `));
  256. if ( counts.conflict ) {
  257. console.log(dedent(`
  258. \x1B[37;1mIt looks like you have some conflicts!\x1B[0m
  259. Run the following command to update license headers:
  260. \x1B[36;1maddlicense sync\x1B[0m
  261. This will begin an interactive license update.
  262. Any time the license doesn't quite match you will
  263. be given the option to replace it or skip the file.
  264. \x1B[90mSee \`addlicense help sync\` for other options.\x1B[0m
  265. You will also be able to choose
  266. "remember for headers matching this one"
  267. if you know the same issue will come up later.
  268. `));
  269. } else if ( counts.missing ) {
  270. console.log(dedent(`
  271. \x1B[37;1mSome missing license headers!\x1B[0m
  272. Run the following command to add the missing license headers:
  273. \x1B[36;1maddlicense sync\x1B[0m
  274. `));
  275. } else {
  276. console.log(dedent(`
  277. \x1B[37;1mNo action to perform!\x1B[0m
  278. Run the following command to do absolutely nothing:
  279. \x1B[36;1maddlicense sync\x1B[0m
  280. `));
  281. }
  282. console.log('');
  283. t.addRow({ situation: 'ok', count: counts.ok },
  284. { color: 'green' });
  285. t.addRow({ situation: 'missing', count: counts.missing },
  286. { color: 'yellow' });
  287. t.addRow({ situation: 'conflict', count: counts.conflict },
  288. { color: 'red' });
  289. t.addRow({ situation: 'unsupported', count: counts.unsupported });
  290. t.printTable();
  291. };
  292. const cmd_sync_fn = async () => {
  293. const comment_parser = CommentParser();
  294. const desired_header = fs.readFileSync(
  295. path_.join(__dirname, '../../doc/license_header.txt'),
  296. 'utf-8',
  297. );
  298. const license_checker = LicenseChecker({
  299. comment_parser,
  300. desired_header,
  301. });
  302. const counts = {
  303. ok: 0,
  304. missing: 0,
  305. conflict: 0,
  306. error: 0,
  307. unsupported: 0,
  308. };
  309. const walk_iterator = walk({
  310. excludes: EXCLUDE_LISTS.NOT_AGPL,
  311. }, '.');
  312. for await ( const value of walk_iterator ) {
  313. if ( value.is_dir ) continue;
  314. process.stdout.write(value.path + ' ... ');
  315. if ( ! license_checker.supports({ filename: value.name }) ) {
  316. process.stdout.write(`\x1B[37;1mUNSUPPORTED\x1B[0m\n`);
  317. counts.unsupported++;
  318. continue;
  319. }
  320. const source = fs.readFileSync(value.path, 'utf-8');
  321. const diff_info = await license_checker.compare({
  322. filename: value.name,
  323. source,
  324. })
  325. if ( ! diff_info ) {
  326. counts.error++;
  327. continue;
  328. }
  329. if ( ! diff_info.has_header ) {
  330. fs.writeFileSync(
  331. value.path,
  332. comment_parser.output_comment({
  333. style: 'block',
  334. filename: value.name,
  335. text: desired_header,
  336. }) +
  337. '\n' +
  338. source
  339. );
  340. continue;
  341. }
  342. if ( diff_info ) {
  343. if ( diff_info.distance !== 0 ) {
  344. counts.conflict++;
  345. process.stdout.write(`\x1B[31;1mCONFLICT\x1B[0m\n`);
  346. process.stdout.write('\x1B[36;1m=======\x1B[0m\n');
  347. process.stdout.write(diff_info.term_diff);
  348. process.stdout.write('\n\x1B[36;1m=======\x1B[0m\n');
  349. const prompt = new enq.Select({
  350. message: 'Select Action',
  351. choices: [
  352. { name: 'skip', message: 'Skip' },
  353. { name: 'replace', message: 'Replace' },
  354. ]
  355. })
  356. const action = await prompt.run();
  357. if ( action === 'skip' ) continue;
  358. const before = source.slice(0, diff_info.range[0]);
  359. const after = source.slice(diff_info.range[1]);
  360. const new_source = before +
  361. comment_parser.output_comment({
  362. style: 'block',
  363. filename: value.name,
  364. text: desired_header,
  365. }) +
  366. after;
  367. fs.writeFileSync(value.path, new_source);
  368. } else {
  369. let cut_diff_info = diff_info;
  370. let cut_source = source;
  371. const cut_header = async () => {
  372. cut_source = cut_source.slice(cut_diff_info.range[1]);
  373. cut_diff_info = await license_checker.compare({
  374. filename: value.name,
  375. source: cut_source,
  376. });
  377. };
  378. await cut_header();
  379. const cut_range = [
  380. diff_info.range[1],
  381. diff_info.range[1],
  382. ];
  383. const cut_diff_infos = [];
  384. while ( cut_diff_info.has_header ) {
  385. cut_diff_infos.push(cut_diff_info);
  386. cut_range[1] += cut_diff_info.range[1];
  387. await cut_header();
  388. }
  389. if ( cut_range[0] !== cut_range[1] ) {
  390. process.stdout.write(`\x1B[31;1mDUPLICATE\x1B[0m\n`);
  391. process.stdout.write('\x1B[36;1m==== KEEP ====\x1B[0m\n');
  392. process.stdout.write(diff_info.term_diff + '\n');
  393. process.stdout.write('\x1B[36;1m==== REMOVE ====\x1B[0m\n');
  394. for ( const diff_info of cut_diff_infos ) {
  395. process.stdout.write(diff_info.term_diff);
  396. }
  397. process.stdout.write('\n\x1B[36;1m=======\x1B[0m\n');
  398. const prompt = new enq.Select({
  399. message: 'Select Action',
  400. choices: [
  401. { name: 'skip', message: 'Skip' },
  402. { name: 'remove', message: 'Remove' },
  403. ]
  404. })
  405. const action = await prompt.run();
  406. if ( action === 'skip' ) continue;
  407. const new_source =
  408. source.slice(0, cut_range[0]) +
  409. source.slice(cut_range[1]);
  410. fs.writeFileSync(value.path, new_source);
  411. }
  412. counts.ok++;
  413. process.stdout.write(`\x1B[32;1mOK\x1B[0m\n`);
  414. }
  415. } else {
  416. console.log('NO COMMENT');
  417. }
  418. }
  419. const { Table } = require('console-table-printer');
  420. const t = new Table({
  421. columns: [
  422. {
  423. title: 'License Header',
  424. name: 'situation', alignment: 'left', color: 'white_bold' },
  425. {
  426. title: 'Number of Files',
  427. name: 'count', alignment: 'right' },
  428. ],
  429. colorMap: {
  430. green: '\x1B[32;1m',
  431. yellow: '\x1B[33;1m',
  432. red: '\x1B[31;1m',
  433. }
  434. });
  435. console.log('');
  436. if ( counts.error > 0 ) {
  437. console.log(`\x1B[31;1mTHERE WERE SOME ERRORS!\x1B[0m`);
  438. console.log('check the log above for the stack trace');
  439. console.log('');
  440. t.addRow({ situation: 'error', count: counts.error },
  441. { color: 'red' });
  442. }
  443. console.log(dedent(`
  444. \x1B[31;1mAny text below is mostly lies!\x1B[0m
  445. This tool is still being developed and most of what's
  446. described is "the plan" rather than a thing that will
  447. actually happen.
  448. \x1B[31;1m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\x1B[0m
  449. `));
  450. if ( counts.conflict ) {
  451. console.log(dedent(`
  452. \x1B[37;1mIt looks like you have some conflicts!\x1B[0m
  453. Run the following command to update license headers:
  454. \x1B[36;1maddlicense sync\x1B[0m
  455. This will begin an interactive license update.
  456. Any time the license doesn't quite match you will
  457. be given the option to replace it or skip the file.
  458. \x1B[90mSee \`addlicense help sync\` for other options.\x1B[0m
  459. You will also be able to choose
  460. "remember for headers matching this one"
  461. if you know the same issue will come up later.
  462. `));
  463. } else if ( counts.missing ) {
  464. console.log(dedent(`
  465. \x1B[37;1mSome missing license headers!\x1B[0m
  466. Run the following command to add the missing license headers:
  467. \x1B[36;1maddlicense sync\x1B[0m
  468. `));
  469. } else {
  470. console.log(dedent(`
  471. \x1B[37;1mNo action to perform!\x1B[0m
  472. Run the following command to do absolutely nothing:
  473. \x1B[36;1maddlicense sync\x1B[0m
  474. `));
  475. }
  476. console.log('');
  477. t.addRow({ situation: 'ok', count: counts.ok },
  478. { color: 'green' });
  479. t.addRow({ situation: 'missing', count: counts.missing },
  480. { color: 'yellow' });
  481. t.addRow({ situation: 'conflict', count: counts.conflict },
  482. { color: 'red' });
  483. t.addRow({ situation: 'unsupported', count: counts.unsupported });
  484. t.printTable();
  485. };
  486. const main = async () => {
  487. const { program } = require('commander');
  488. const helptext = dedent(`
  489. Usage: usage text
  490. `);
  491. const run_command = async ({ cmd, cmd_fn }) => {
  492. const options = {
  493. program: program.opts(),
  494. command: cmd.opts(),
  495. };
  496. console.log('options', options);
  497. if ( ! fs.existsSync(options.program.config) ) {
  498. // TODO: configuration wizard
  499. fs.writeFileSync(options.program.config, '');
  500. }
  501. await cmd_fn({ options });
  502. };
  503. program
  504. .name('addlicense')
  505. .option('-c, --config', 'configuration file', 'addlicense.yml')
  506. .addHelpText('before', helptext)
  507. ;
  508. const cmd_check = program.command('check')
  509. .description('check license headers')
  510. .option('-n, --non-interactive', 'disable prompting')
  511. .action(() => {
  512. run_command({ cmd: cmd_check, cmd_fn: cmd_check_fn });
  513. })
  514. const cmd_sync = program.command('sync')
  515. .description('synchronize files with license header rules')
  516. .option('-n, --non-interactive', 'disable prompting')
  517. .action(() => {
  518. run_command({ cmd: cmd_sync, cmd_fn: cmd_sync_fn })
  519. })
  520. program.parse(process.argv);
  521. };
  522. if ( require.main === module ) {
  523. main();
  524. }