mirror of
https://github.com/HeyPuter/puter
synced 2024-11-14 22:06:00 +00:00
594 lines
19 KiB
JavaScript
594 lines
19 KiB
JavaScript
/*
|
|
* Copyright (C) 2024 Puter Technologies Inc.
|
|
*
|
|
* This file is part of Puter.
|
|
*
|
|
* Puter is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
const levenshtein = require('js-levenshtein');
|
|
const DiffMatchPatch = require('diff-match-patch');
|
|
const enq = require('enquirer');
|
|
const dmp = new DiffMatchPatch();
|
|
const dedent = require('dedent');
|
|
|
|
const { walk, EXCLUDE_LISTS } = require('file-walker');
|
|
const { CommentParser } = require('../comment-parser/main');
|
|
|
|
const fs = require('fs');
|
|
const path_ = require('path');
|
|
|
|
const CompareFn = ({ header1, header2, distance_only = false }) => {
|
|
|
|
// Calculate Levenshtein distance
|
|
const distance = levenshtein(header1, header2);
|
|
// console.log(`Levenshtein distance: ${distance}`);
|
|
|
|
if ( distance_only ) return { distance };
|
|
|
|
// Generate diffs using diff-match-patch
|
|
const diffs = dmp.diff_main(header1, header2);
|
|
dmp.diff_cleanupSemantic(diffs);
|
|
|
|
let term_diff = '';
|
|
|
|
// Manually format diffs for terminal display
|
|
diffs.forEach(([type, text]) => {
|
|
switch (type) {
|
|
case DiffMatchPatch.DIFF_INSERT:
|
|
term_diff += `\x1b[32m${text}\x1b[0m`; // Green for insertions
|
|
break;
|
|
case DiffMatchPatch.DIFF_DELETE:
|
|
term_diff += `\x1b[31m${text}\x1b[0m`; // Red for deletions
|
|
break;
|
|
case DiffMatchPatch.DIFF_EQUAL:
|
|
term_diff += text; // No color for equal parts
|
|
break;
|
|
}
|
|
});
|
|
|
|
return {
|
|
distance,
|
|
term_diff,
|
|
};
|
|
}
|
|
|
|
const LicenseChecker = ({
|
|
comment_parser,
|
|
desired_header,
|
|
}) => {
|
|
const supports = ({ filename }) => {
|
|
return comment_parser.supports({ filename });
|
|
};
|
|
const compare = async ({ filename, source }) => {
|
|
const headers = await comment_parser.extract_top_comments(
|
|
{ filename, source });
|
|
const headers_lines = headers.map(h => h.lines);
|
|
|
|
if ( headers.length < 1 ) {
|
|
return {
|
|
has_header: false,
|
|
};
|
|
}
|
|
|
|
// console.log('headers', headers);
|
|
|
|
let top = 0;
|
|
let bottom = 0;
|
|
let current_distance = Number.MAX_SAFE_INTEGER;
|
|
|
|
// "wah"
|
|
for ( let i=1 ; i <= headers.length ; i++ ) {
|
|
const combined = headers_lines.slice(top, i).flat();
|
|
const combined_txt = combined.join('\n');
|
|
const { distance } =
|
|
CompareFn({
|
|
header1: desired_header,
|
|
header2: combined_txt,
|
|
distance_only: true,
|
|
});
|
|
if ( distance < current_distance ) {
|
|
current_distance = distance;
|
|
bottom = i;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
// "woop"
|
|
for ( let i=1 ; i < headers.length ; i++ ) {
|
|
const combined = headers_lines.slice(i, bottom).flat();
|
|
const combined_txt = combined.join('\n');
|
|
const { distance } =
|
|
CompareFn({
|
|
header1: desired_header,
|
|
header2: combined_txt,
|
|
distance_only: true,
|
|
});
|
|
if ( distance < current_distance ) {
|
|
current_distance = distance;
|
|
top = i;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// console.log('headers', headers);
|
|
|
|
const combined = headers_lines.slice(top, bottom).flat();
|
|
const combined_txt = combined.join('\n');
|
|
|
|
const diff_info = CompareFn({
|
|
header1: desired_header,
|
|
header2: combined_txt,
|
|
})
|
|
|
|
if ( diff_info.distance > 0.7*desired_header.length ) {
|
|
return {
|
|
has_header: false,
|
|
};
|
|
}
|
|
|
|
diff_info.range = [
|
|
headers[top].range[0],
|
|
headers[bottom-1].range[1],
|
|
];
|
|
|
|
diff_info.has_header = true;
|
|
|
|
return diff_info;
|
|
};
|
|
return {
|
|
compare,
|
|
supports,
|
|
};
|
|
};
|
|
|
|
const license_check_test = async ({ options }) => {
|
|
const comment_parser = CommentParser();
|
|
const license_checker = LicenseChecker({
|
|
comment_parser,
|
|
desired_header: fs.readFileSync(
|
|
path_.join(__dirname, '../../doc/license_header.txt'),
|
|
'utf-8',
|
|
),
|
|
});
|
|
|
|
const walk_iterator = walk({
|
|
excludes: EXCLUDE_LISTS.NOT_AGPL,
|
|
}, path_.join(__dirname, '../..'));
|
|
for await ( const value of walk_iterator ) {
|
|
if ( value.is_dir ) continue;
|
|
if ( value.name !== 'dev-console-ui-utils.js' ) continue;
|
|
console.log(value.path);
|
|
const source = fs.readFileSync(value.path, 'utf-8');
|
|
const diff_info = await license_checker.compare({
|
|
filename: value.name,
|
|
source,
|
|
})
|
|
if ( diff_info ) {
|
|
process.stdout.write('\x1B[36;1m=======\x1B[0m\n');
|
|
process.stdout.write(diff_info.term_diff);
|
|
process.stdout.write('\n\x1B[36;1m=======\x1B[0m\n');
|
|
// console.log('headers', headers);
|
|
} else {
|
|
console.log('NO COMMENT');
|
|
}
|
|
|
|
console.log('RANGE', diff_info.range)
|
|
|
|
const new_comment = comment_parser.output_comment({
|
|
filename: value.name,
|
|
style: 'block',
|
|
text: 'some text\nto display'
|
|
});
|
|
|
|
console.log('NEW COMMENT?', new_comment);
|
|
}
|
|
};
|
|
|
|
const cmd_check_fn = async () => {
|
|
const comment_parser = CommentParser();
|
|
const license_checker = LicenseChecker({
|
|
comment_parser,
|
|
desired_header: fs.readFileSync(
|
|
path_.join(__dirname, '../../doc/license_header.txt'),
|
|
'utf-8',
|
|
),
|
|
});
|
|
|
|
const counts = {
|
|
ok: 0,
|
|
missing: 0,
|
|
conflict: 0,
|
|
error: 0,
|
|
unsupported: 0,
|
|
};
|
|
|
|
const walk_iterator = walk({
|
|
excludes: EXCLUDE_LISTS.NOT_AGPL,
|
|
}, path_.join(__dirname, '../..'));
|
|
for await ( const value of walk_iterator ) {
|
|
if ( value.is_dir ) continue;
|
|
|
|
process.stdout.write(value.path + ' ... ');
|
|
|
|
if ( ! license_checker.supports({ filename: value.name }) ) {
|
|
process.stdout.write(`\x1B[37;1mUNSUPPORTED\x1B[0m\n`);
|
|
counts.unsupported++;
|
|
continue;
|
|
}
|
|
|
|
const source = fs.readFileSync(value.path, 'utf-8');
|
|
const diff_info = await license_checker.compare({
|
|
filename: value.name,
|
|
source,
|
|
})
|
|
if ( ! diff_info ) {
|
|
counts.error++;
|
|
continue;
|
|
}
|
|
if ( ! diff_info.has_header ) {
|
|
counts.missing++;
|
|
process.stdout.write(`\x1B[33;1mMISSING\x1B[0m\n`);
|
|
continue;
|
|
}
|
|
if ( diff_info ) {
|
|
if ( diff_info.distance !== 0 ) {
|
|
counts.conflict++;
|
|
process.stdout.write(`\x1B[31;1mCONFLICT\x1B[0m\n`);
|
|
} else {
|
|
counts.ok++;
|
|
process.stdout.write(`\x1B[32;1mOK\x1B[0m\n`);
|
|
}
|
|
} else {
|
|
console.log('NO COMMENT');
|
|
}
|
|
}
|
|
|
|
const { Table } = require('console-table-printer');
|
|
const t = new Table({
|
|
columns: [
|
|
{
|
|
title: 'License Header',
|
|
name: 'situation', alignment: 'left', color: 'white_bold' },
|
|
{
|
|
title: 'Number of Files',
|
|
name: 'count', alignment: 'right' },
|
|
],
|
|
colorMap: {
|
|
green: '\x1B[32;1m',
|
|
yellow: '\x1B[33;1m',
|
|
red: '\x1B[31;1m',
|
|
}
|
|
});
|
|
|
|
console.log('');
|
|
|
|
if ( counts.error > 0 ) {
|
|
console.log(`\x1B[31;1mTHERE WERE SOME ERRORS!\x1B[0m`);
|
|
console.log('check the log above for the stack trace');
|
|
console.log('');
|
|
t.addRow({ situation: 'error', count: counts.error },
|
|
{ color: 'red' });
|
|
}
|
|
|
|
console.log(dedent(`
|
|
\x1B[31;1mAny text below is mostly lies!\x1B[0m
|
|
This tool is still being developed and most of what's
|
|
described is "the plan" rather than a thing that will
|
|
actually happen.
|
|
\x1B[31;1m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\x1B[0m
|
|
`));
|
|
|
|
if ( counts.conflict ) {
|
|
console.log(dedent(`
|
|
\x1B[37;1mIt looks like you have some conflicts!\x1B[0m
|
|
Run the following command to update license headers:
|
|
|
|
\x1B[36;1maddlicense sync\x1B[0m
|
|
|
|
This will begin an interactive license update.
|
|
Any time the license doesn't quite match you will
|
|
be given the option to replace it or skip the file.
|
|
\x1B[90mSee \`addlicense help sync\` for other options.\x1B[0m
|
|
|
|
You will also be able to choose
|
|
"remember for headers matching this one"
|
|
if you know the same issue will come up later.
|
|
`));
|
|
} else if ( counts.missing ) {
|
|
console.log(dedent(`
|
|
\x1B[37;1mSome missing license headers!\x1B[0m
|
|
Run the following command to add the missing license headers:
|
|
|
|
\x1B[36;1maddlicense sync\x1B[0m
|
|
`));
|
|
} else {
|
|
console.log(dedent(`
|
|
\x1B[37;1mNo action to perform!\x1B[0m
|
|
Run the following command to do absolutely nothing:
|
|
|
|
\x1B[36;1maddlicense sync\x1B[0m
|
|
`));
|
|
}
|
|
|
|
console.log('');
|
|
|
|
t.addRow({ situation: 'ok', count: counts.ok },
|
|
{ color: 'green' });
|
|
t.addRow({ situation: 'missing', count: counts.missing },
|
|
{ color: 'yellow' });
|
|
t.addRow({ situation: 'conflict', count: counts.conflict },
|
|
{ color: 'red' });
|
|
t.addRow({ situation: 'unsupported', count: counts.unsupported });
|
|
t.printTable();
|
|
};
|
|
|
|
const cmd_sync_fn = async () => {
|
|
const comment_parser = CommentParser();
|
|
const desired_header = fs.readFileSync(
|
|
path_.join(__dirname, '../../doc/license_header.txt'),
|
|
'utf-8',
|
|
);
|
|
const license_checker = LicenseChecker({
|
|
comment_parser,
|
|
desired_header,
|
|
});
|
|
|
|
const counts = {
|
|
ok: 0,
|
|
missing: 0,
|
|
conflict: 0,
|
|
error: 0,
|
|
unsupported: 0,
|
|
};
|
|
|
|
const walk_iterator = walk({
|
|
excludes: EXCLUDE_LISTS.NOT_AGPL,
|
|
}, '.');
|
|
for await ( const value of walk_iterator ) {
|
|
if ( value.is_dir ) continue;
|
|
|
|
process.stdout.write(value.path + ' ... ');
|
|
|
|
if ( ! license_checker.supports({ filename: value.name }) ) {
|
|
process.stdout.write(`\x1B[37;1mUNSUPPORTED\x1B[0m\n`);
|
|
counts.unsupported++;
|
|
continue;
|
|
}
|
|
|
|
const source = fs.readFileSync(value.path, 'utf-8');
|
|
const diff_info = await license_checker.compare({
|
|
filename: value.name,
|
|
source,
|
|
})
|
|
if ( ! diff_info ) {
|
|
counts.error++;
|
|
continue;
|
|
}
|
|
if ( ! diff_info.has_header ) {
|
|
fs.writeFileSync(
|
|
value.path,
|
|
comment_parser.output_comment({
|
|
style: 'block',
|
|
filename: value.name,
|
|
text: desired_header,
|
|
}) +
|
|
'\n' +
|
|
source
|
|
);
|
|
continue;
|
|
}
|
|
if ( diff_info ) {
|
|
if ( diff_info.distance !== 0 ) {
|
|
counts.conflict++;
|
|
process.stdout.write(`\x1B[31;1mCONFLICT\x1B[0m\n`);
|
|
process.stdout.write('\x1B[36;1m=======\x1B[0m\n');
|
|
process.stdout.write(diff_info.term_diff);
|
|
process.stdout.write('\n\x1B[36;1m=======\x1B[0m\n');
|
|
const prompt = new enq.Select({
|
|
message: 'Select Action',
|
|
choices: [
|
|
{ name: 'skip', message: 'Skip' },
|
|
{ name: 'replace', message: 'Replace' },
|
|
]
|
|
})
|
|
const action = await prompt.run();
|
|
if ( action === 'skip' ) continue;
|
|
const before = source.slice(0, diff_info.range[0]);
|
|
const after = source.slice(diff_info.range[1]);
|
|
const new_source = before +
|
|
comment_parser.output_comment({
|
|
style: 'block',
|
|
filename: value.name,
|
|
text: desired_header,
|
|
}) +
|
|
after;
|
|
fs.writeFileSync(value.path, new_source);
|
|
} else {
|
|
let cut_diff_info = diff_info;
|
|
let cut_source = source;
|
|
const cut_header = async () => {
|
|
cut_source = cut_source.slice(cut_diff_info.range[1]);
|
|
cut_diff_info = await license_checker.compare({
|
|
filename: value.name,
|
|
source: cut_source,
|
|
});
|
|
};
|
|
await cut_header();
|
|
const cut_range = [
|
|
diff_info.range[1],
|
|
diff_info.range[1],
|
|
];
|
|
const cut_diff_infos = [];
|
|
while ( cut_diff_info.has_header ) {
|
|
cut_diff_infos.push(cut_diff_info);
|
|
cut_range[1] += cut_diff_info.range[1];
|
|
await cut_header();
|
|
}
|
|
if ( cut_range[0] !== cut_range[1] ) {
|
|
process.stdout.write(`\x1B[31;1mDUPLICATE\x1B[0m\n`);
|
|
process.stdout.write('\x1B[36;1m==== KEEP ====\x1B[0m\n');
|
|
process.stdout.write(diff_info.term_diff + '\n');
|
|
process.stdout.write('\x1B[36;1m==== REMOVE ====\x1B[0m\n');
|
|
for ( const diff_info of cut_diff_infos ) {
|
|
process.stdout.write(diff_info.term_diff);
|
|
}
|
|
process.stdout.write('\n\x1B[36;1m=======\x1B[0m\n');
|
|
const prompt = new enq.Select({
|
|
message: 'Select Action',
|
|
choices: [
|
|
{ name: 'skip', message: 'Skip' },
|
|
{ name: 'remove', message: 'Remove' },
|
|
]
|
|
})
|
|
const action = await prompt.run();
|
|
if ( action === 'skip' ) continue;
|
|
const new_source =
|
|
source.slice(0, cut_range[0]) +
|
|
source.slice(cut_range[1]);
|
|
fs.writeFileSync(value.path, new_source);
|
|
}
|
|
counts.ok++;
|
|
process.stdout.write(`\x1B[32;1mOK\x1B[0m\n`);
|
|
}
|
|
} else {
|
|
console.log('NO COMMENT');
|
|
}
|
|
}
|
|
|
|
const { Table } = require('console-table-printer');
|
|
const t = new Table({
|
|
columns: [
|
|
{
|
|
title: 'License Header',
|
|
name: 'situation', alignment: 'left', color: 'white_bold' },
|
|
{
|
|
title: 'Number of Files',
|
|
name: 'count', alignment: 'right' },
|
|
],
|
|
colorMap: {
|
|
green: '\x1B[32;1m',
|
|
yellow: '\x1B[33;1m',
|
|
red: '\x1B[31;1m',
|
|
}
|
|
});
|
|
|
|
console.log('');
|
|
|
|
if ( counts.error > 0 ) {
|
|
console.log(`\x1B[31;1mTHERE WERE SOME ERRORS!\x1B[0m`);
|
|
console.log('check the log above for the stack trace');
|
|
console.log('');
|
|
t.addRow({ situation: 'error', count: counts.error },
|
|
{ color: 'red' });
|
|
}
|
|
|
|
console.log(dedent(`
|
|
\x1B[31;1mAny text below is mostly lies!\x1B[0m
|
|
This tool is still being developed and most of what's
|
|
described is "the plan" rather than a thing that will
|
|
actually happen.
|
|
\x1B[31;1m^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\x1B[0m
|
|
`));
|
|
|
|
if ( counts.conflict ) {
|
|
console.log(dedent(`
|
|
\x1B[37;1mIt looks like you have some conflicts!\x1B[0m
|
|
Run the following command to update license headers:
|
|
|
|
\x1B[36;1maddlicense sync\x1B[0m
|
|
|
|
This will begin an interactive license update.
|
|
Any time the license doesn't quite match you will
|
|
be given the option to replace it or skip the file.
|
|
\x1B[90mSee \`addlicense help sync\` for other options.\x1B[0m
|
|
|
|
You will also be able to choose
|
|
"remember for headers matching this one"
|
|
if you know the same issue will come up later.
|
|
`));
|
|
} else if ( counts.missing ) {
|
|
console.log(dedent(`
|
|
\x1B[37;1mSome missing license headers!\x1B[0m
|
|
Run the following command to add the missing license headers:
|
|
|
|
\x1B[36;1maddlicense sync\x1B[0m
|
|
`));
|
|
} else {
|
|
console.log(dedent(`
|
|
\x1B[37;1mNo action to perform!\x1B[0m
|
|
Run the following command to do absolutely nothing:
|
|
|
|
\x1B[36;1maddlicense sync\x1B[0m
|
|
`));
|
|
}
|
|
|
|
console.log('');
|
|
|
|
t.addRow({ situation: 'ok', count: counts.ok },
|
|
{ color: 'green' });
|
|
t.addRow({ situation: 'missing', count: counts.missing },
|
|
{ color: 'yellow' });
|
|
t.addRow({ situation: 'conflict', count: counts.conflict },
|
|
{ color: 'red' });
|
|
t.addRow({ situation: 'unsupported', count: counts.unsupported });
|
|
t.printTable();
|
|
};
|
|
|
|
const main = async () => {
|
|
const { program } = require('commander');
|
|
const helptext = dedent(`
|
|
Usage: usage text
|
|
`);
|
|
|
|
const run_command = async ({ cmd, cmd_fn }) => {
|
|
const options = {
|
|
program: program.opts(),
|
|
command: cmd.opts(),
|
|
};
|
|
console.log('options', options);
|
|
|
|
if ( ! fs.existsSync(options.program.config) ) {
|
|
// TODO: configuration wizard
|
|
fs.writeFileSync(options.program.config, '');
|
|
}
|
|
|
|
await cmd_fn({ options });
|
|
};
|
|
|
|
program
|
|
.name('addlicense')
|
|
.option('-c, --config', 'configuration file', 'addlicense.yml')
|
|
.addHelpText('before', helptext)
|
|
;
|
|
const cmd_check = program.command('check')
|
|
.description('check license headers')
|
|
.option('-n, --non-interactive', 'disable prompting')
|
|
.action(() => {
|
|
run_command({ cmd: cmd_check, cmd_fn: cmd_check_fn });
|
|
})
|
|
const cmd_sync = program.command('sync')
|
|
.description('synchronize files with license header rules')
|
|
.option('-n, --non-interactive', 'disable prompting')
|
|
.action(() => {
|
|
run_command({ cmd: cmd_sync, cmd_fn: cmd_sync_fn })
|
|
})
|
|
program.parse(process.argv);
|
|
|
|
};
|
|
|
|
if ( require.main === module ) {
|
|
main();
|
|
} |