diff --git a/README.md b/README.md index 2ce35f8..f4a1d97 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ $ npx myvc [command] Execute *myvc* with the desired command. ```text -$ myvc [-w|--workspace] [-r|--remote] [-d|--debug] [-h|--help] command [args] +$ [npx] myvc [-w|--workspace ] [-r|--remote ] [-d|--debug] [-h|--help] [] ``` The default workspace directory is the current working directory and unless @@ -48,8 +48,8 @@ Database versioning commands: Local server management commands: - * **dump**: Export database structure and fixtures from *production*. - * **run**: Build and starts local database server container. + * **dump**: Export database structure and fixtures. + * **run**: Build and start local database server container. * **start**: Start local database server container. Each command can have its own specific commandline options. @@ -72,32 +72,26 @@ Incorporates database routine changes into workspace. $ myvc pull [remote] [-f|--force] [-c|--checkout] ``` -When *checkout* option is provided, it does the following steps: +When *checkout* option is provided, it does the following before export: -1. Saves the current HEAD. -2. Check out to the last database push commit (saved in versioning tables). -3. Creates and checkout to a new branch. -4. Exports database routines. -5. Commits the new changes. -6. Checkout to the original HEAD. -7. Merge the new branch into. -8. Let the user deal with merge conflicts. +1. Get the last database push commit (saved in versioning tables). +2. Creates and checkout to a new branch based in database commit. ### push Applies versions and routine changes into database. ```text -$ myvc push [remote] [-f|--force] [-u|--user] +$ myvc push [] [-f|--force] [-u|--user] ``` ### version Creates a new version folder, when name is not specified it generates a random -name mixing color with plant name. +name mixing a color with a plant name. ```text -$ myvc version [name] +$ myvc version [] ``` ## Local server commands @@ -105,10 +99,10 @@ $ myvc version [name] ### dump Exports database structure and fixtures from remote into hidden files located -in *dump* folder. +in *dump* folder. If no remote is specified *production* is used. ```text -$ myvc dump [remote] +$ myvc dump [] ``` ### run @@ -134,7 +128,7 @@ $ myvc start ## Basic information -First of all you have to initalize your workspace. +First of all you have to initalize the workspace. ```text $ myvc init @@ -156,9 +150,9 @@ remotes/[remote].ini ### Routines -Routines should be placed inside *routines* folder. All objects that have -PL/SQL code are considered routines. It includes events, functions, procedures, -triggers and views with the following structure. +Routines are placed inside *routines* folder. All objects that have PL/SQL code +are considered routines. It includes events, functions, procedures, triggers +and views with the following structure. ```text routines @@ -193,10 +187,11 @@ Don't place your PL/SQL objects here, use the routines folder! ### Local server -The local server is created as a MariaDB Docker container using the base dump created with the *dump* command plus pushing local versions and changed +The local server is created as a MariaDB Docker container using the base dump +created with the *dump* command plus pushing local versions and changed routines. -## Dumps +### Dumps You can create your local fixture and structure files. @@ -206,18 +201,18 @@ You can create your local fixture and structure files. ## Why The main reason for starting this project it's because there are no fully free -and opensource migration tools available that allow versioning database routines -with an standard CVS system as if they were normal application code. +and open source migration tools available that allow versioning database +routines with an standard CVS system as if they were normal application code. -Also, the existing tools are too complex and require too much knowledge to start -a small project. +Also, the existing tools are too complex and require too much knowledge to +start a small project. ## Todo -* Improve *help* option for commands. -* Allow to specify a custom workspace subdirectory inside project directory. -* Create a lock during push to avoid collisions. +* Update routines shasum when push. * Use a custom *Dockerfile* for local database container. +* Console logging via events. +* Lock version table row when pushing. ## Built With diff --git a/docker.js b/docker.js index d84665d..2c14fc7 100644 --- a/docker.js +++ b/docker.js @@ -1,5 +1,6 @@ const spawn = require('child_process').spawn; const execFile = require('child_process').execFile; +const camelToSnake = require('./lib').camelToSnake; const docker = { async run(image, commandArgs, options, execOptions) { @@ -120,9 +121,5 @@ class Container { } } -function camelToSnake(str) { - return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`); -} - module.exports = docker; module.exports.Container = Container; diff --git a/lib.js b/lib.js new file mode 100644 index 0000000..65220b9 --- /dev/null +++ b/lib.js @@ -0,0 +1,6 @@ + +function camelToSnake(str) { + return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`); +} + +module.exports.camelToSnake = camelToSnake; diff --git a/myvc-dump.js b/myvc-dump.js index e2d77b6..03258b2 100644 --- a/myvc-dump.js +++ b/myvc-dump.js @@ -4,13 +4,16 @@ const fs = require('fs-extra'); const path = require('path'); const docker = require('./docker'); -/** - * Dumps structure and fixtures from remote. - */ class Dump { + get usage() { + return { + description: 'Dumps structure and fixtures from remote', + operand: 'remote' + }; + } + get localOpts() { return { - operand: 'remote', default: { remote: 'production' } @@ -20,7 +23,7 @@ class Dump { async run(myvc, opts) { const conn = await myvc.dbConnect(); - const dumpDir = `${opts.workspace}/dump`; + const dumpDir = `${opts.myvcDir}/dump`; if (!await fs.pathExists(dumpDir)) await fs.mkdir(dumpDir); @@ -82,7 +85,7 @@ class Dump { async dockerRun(command, args, execOptions) { const commandArgs = [command].concat(args); await docker.run('myvc/client', commandArgs, { - volume: `${this.opts.workspace}:/workspace`, + volume: `${this.opts.myvcDir}:/workspace`, rm: true }, execOptions); } diff --git a/myvc-init.js b/myvc-init.js index 03f86a0..99c245c 100755 --- a/myvc-init.js +++ b/myvc-init.js @@ -3,11 +3,17 @@ const MyVC = require('./myvc'); const fs = require('fs-extra'); class Init { + get usage() { + return { + description: 'Initialize an empty workspace' + }; + } + async run(myvc, opts) { const templateDir = `${__dirname}/template`; const templates = await fs.readdir(templateDir); for (let template of templates) { - const dst = `${opts.workspace}/${template}`; + const dst = `${opts.myvcDir}/${template}`; if (!await fs.pathExists(dst)) await fs.copy(`${templateDir}/${template}`, dst); } diff --git a/myvc-pull.js b/myvc-pull.js index 774aef2..1161a78 100755 --- a/myvc-pull.js +++ b/myvc-pull.js @@ -6,10 +6,20 @@ const shajs = require('sha.js'); const nodegit = require('nodegit'); class Pull { + get usage() { + return { + description: 'Incorporate database routine changes into workspace', + params: { + force: 'Do it even if there are local changes', + checkout: 'Move to same database commit before pull' + }, + operand: 'remote' + }; + } + get localOpts() { return { - operand: 'remote', - alias: { + boolean: { force: 'f', checkout: 'c' } @@ -80,7 +90,7 @@ class Pull { for (const exporter of exporters) await exporter.init(); - const exportDir = `${opts.workspace}/routines`; + const exportDir = `${opts.myvcDir}/routines`; if (!await fs.pathExists(exportDir)) await fs.mkdir(exportDir); @@ -88,7 +98,7 @@ class Pull { let newShaSums = {}; let oldShaSums; - const shaFile = `${opts.workspace}/.shasums.json`; + const shaFile = `${opts.myvcDir}/.shasums.json`; if (await fs.pathExists(shaFile)) oldShaSums = JSON.parse(await fs.readFile(shaFile, 'utf8')); diff --git a/myvc-push.js b/myvc-push.js index 7bddcf5..8124ea3 100644 --- a/myvc-push.js +++ b/myvc-push.js @@ -5,15 +5,22 @@ const nodegit = require('nodegit'); /** * Pushes changes to remote. - * - * @property {Boolean} force Answer yes to all questions - * @property {Boolean} user Whether to change current user version */ class Push { + get usage() { + return { + description: 'Apply changes into database', + params: { + force: 'Answer yes to all questions', + user: 'Update the user version instead, for shared databases' + }, + operand: 'remote' + }; + } + get localOpts() { return { - operand: 'remote', - alias: { + boolean: { force: 'f', user: 'u' } @@ -24,6 +31,25 @@ class Push { const conn = await myvc.dbConnect(); this.conn = conn; + // Obtain exclusive lock + + const [[row]] = await conn.query( + `SELECT GET_LOCK('myvc_push', 30) getLock`); + + if (!row.getLock) { + let isUsed = 0; + + if (row.getLock == 0) { + const [[row]] = await conn.query( + `SELECT IS_USED_LOCK('myvc_push') isUsed`); + isUsed = row.isUsed; + } + + throw new Error(`Cannot obtain exclusive lock, used by connection ${isUsed}`); + } + + // Get database version + const version = await myvc.fetchDbVersion() || {}; console.log( @@ -33,7 +59,7 @@ class Push { ); if (!version.number) - version.number = '00000'; + version.number = String('0').padStart(opts.versionDigits, '0'); if (!/^[0-9]*$/.test(version.number)) throw new Error('Wrong database version'); @@ -59,6 +85,8 @@ class Push { version.gitCommit = userVersion.gitCommit; } + // Prevent push to production by mistake + if (opts.remote == 'production') { console.log( '\n ( ( ) ( ( ) ) ' @@ -88,11 +116,13 @@ class Push { } } + // Apply versions + console.log('Applying versions.'); const pushConn = await myvc.createConnection(); let nChanges = 0; - const versionsDir = `${opts.workspace}/versions`; + const versionsDir = `${opts.myvcDir}/versions`; function logVersion(type, version, name) { console.log('', type.bold, `[${version.bold}]`, name); @@ -105,21 +135,25 @@ class Push { if (versionDir == 'README.md') continue; - const match = versionDir.match(/^([0-9]{5})-([a-zA-Z0-9]+)?$/); + const match = versionDir.match(/^([0-9])-([a-zA-Z0-9]+)?$/); if (!match) { logVersion('[W]'.yellow, '?????', versionDir); continue; } - const dirVersion = match[1]; + const versionNumber = match[1]; const versionName = match[2]; - if (version.number >= dirVersion) { - logVersion('[I]'.blue, dirVersion, versionName); + if (versionNumber.length != version.number.length) { + logVersion('[W]'.yellow, '*****', versionDir); + continue; + } + if (version.number >= versionNumber) { + logVersion('[I]'.blue, versionNumber, versionName); continue; } - logVersion('[+]'.green, dirVersion, versionName); + logVersion('[+]'.green, versionNumber, versionName); const scriptsDir = `${versionsDir}/${versionDir}`; const scripts = await fs.readdir(scriptsDir); @@ -134,10 +168,12 @@ class Push { nChanges++; } - await this.updateVersion(nChanges, 'number', dirVersion); + await this.updateVersion(nChanges, 'number', versionNumber); } } + // Apply routines + console.log('Applying changed routines.'); let nRoutines = 0; @@ -146,12 +182,6 @@ class Push { : await myvc.cachedChanges(); changes = this.parseChanges(changes); - await conn.query( - `CREATE TEMPORARY TABLE tProcsPriv - ENGINE = MEMORY - SELECT * FROM mysql.procs_priv LIMIT 0` - ); - const routines = []; for (const change of changes) if (change.isRoutine) @@ -171,19 +201,32 @@ class Push { } for (const change of changes) { - const fullPath = `${opts.workspace}/routines/${change.path}.sql`; + const fullPath = `${opts.myvcDir}/routines/${change.path}.sql`; const exists = await fs.pathExists(fullPath); + const actionMsg = exists ? '[+]'.green : '[-]'.red; const typeMsg = `[${change.type.abbr}]`[change.type.color]; - console.log('', actionMsg.bold, typeMsg.bold, change.fullName); try { const scapedSchema = pushConn.escapeId(change.schema, true); if (exists) { - await pushConn.query(`USE ${scapedSchema}`); + if (change.type.name = 'VIEW') + await pushConn.query(`USE ${scapedSchema}`); + await this.queryFromFile(pushConn, `routines/${change.path}.sql`); + + if (change.isRoutine) { + await conn.query( + `INSERT IGNORE INTO mysql.procs_priv + SELECT * FROM tProcsPriv + WHERE Db = ? + AND Routine_name = ? + AND Routine_type = ?`, + [change.schema, change.name, change.type.name] + ); + } } else { const escapedName = scapedSchema + '.' + @@ -203,20 +246,15 @@ class Push { } if (routines.length) { - await conn.query( - `INSERT IGNORE INTO mysql.procs_priv - SELECT * FROM tProcsPriv` - ); - await conn.query( - `DROP TEMPORARY TABLE tProcsPriv` - ); + await conn.query(`DROP TEMPORARY TABLE tProcsPriv`); + await conn.query('FLUSH PRIVILEGES'); } + // Update and release + await pushConn.end(); if (nRoutines > 0) { - await conn.query('FLUSH PRIVILEGES'); - console.log(` -> ${nRoutines} routines have changed.`); } else console.log(` -> No routines changed.`); @@ -226,6 +264,8 @@ class Push { if (version.gitCommit !== head.sha()) await this.updateVersion(nRoutines, 'gitCommit', head.sha()); + + await conn.query(`DO RELEASE_LOCK('myvc_push')`); } parseChanges(changes) { @@ -395,6 +435,11 @@ const typeMap = { }, }; +const routineTypes = new Set([ + 'FUNCTION', + 'PROCEDURE' +]); + class Routine { constructor(change) { const path = change.path; @@ -411,7 +456,7 @@ class Routine { schema, name, fullName: `${schema}.${name}`, - isRoutine: ['FUNC', 'PROC'].indexOf(type.abbr) !== -1 + isRoutine: routineTypes.has(type.abbr) }); } } diff --git a/myvc-run.js b/myvc-run.js index 5da76ed..026da01 100644 --- a/myvc-run.js +++ b/myvc-run.js @@ -11,14 +11,21 @@ const Server = require('./server/server'); * image when fixtures have been modified or when the day on which the * image was built is different to today. Some workarounds have been used * to avoid a bug with OverlayFS driver on MacOS. - * - * @property {Boolean} ci Continuous integration environment - * @property {Boolean} random Whether to use a random container name */ class Run { + get usage() { + return { + description: 'Build and start local database server container', + params: { + ci: 'Workaround for continuous integration system', + random: 'Whether to use a random container name or port' + } + }; + } + get localOpts() { return { - alias: { + boolean: { ci: 'c', random: 'r' } @@ -26,7 +33,7 @@ class Run { } async run(myvc, opts) { - const dumpDir = `${opts.workspace}/dump`; + const dumpDir = `${opts.myvcDir}/dump`; if (!await fs.pathExists(`${dumpDir}/.dump.sql`)) throw new Error('To run local database you have to create a dump first'); @@ -71,7 +78,7 @@ class Run { const day = pad(today.getDate()); const stamp = `${year}-${month}-${day}`; - await docker.build(opts.workspace, { + await docker.build(opts.myvcDir, { tag: opts.code, file: `${dockerfilePath}.dump`, buildArg: `STAMP=${stamp}` diff --git a/myvc-start.js b/myvc-start.js index e6ad9f0..ea8b5c9 100644 --- a/myvc-start.js +++ b/myvc-start.js @@ -11,6 +11,12 @@ const Run = require('./myvc-run'); * version of it. */ class Start { + get usage() { + return { + description: 'Start local database server container' + }; + } + async run(myvc, opts) { const ct = new Container(opts.code); let status; diff --git a/myvc-version.js b/myvc-version.js index 99e4c6d..33357ae 100644 --- a/myvc-version.js +++ b/myvc-version.js @@ -6,11 +6,19 @@ const fs = require('fs-extra'); * Creates a new version. */ class Version { - mainOpt = 'name'; + get usage() { + return { + description: 'Creates a new version', + params: { + name: 'Name for the new version' + }, + operand: 'name' + }; + } + get localOpts() { return { - operand: 'name', - name: { + string: { name: 'n' }, default: { @@ -20,35 +28,52 @@ class Version { } async run(myvc, opts) { - const verionsDir =`${opts.workspace}/versions`; let versionDir; - let versionName = opts.name; // Fetch last version number const conn = await myvc.dbConnect(); - const version = await myvc.fetchDbVersion() || {}; try { await conn.query('START TRANSACTION'); const [[row]] = await conn.query( - `SELECT lastNumber FROM version WHERE code = ? FOR UPDATE`, + `SELECT number, lastNumber + FROM version + WHERE code = ? + FOR UPDATE`, [opts.code] ); - const lastVersion = row && row.lastNumber; + const number = row && row.number; + const lastNumber = row && row.lastNumber; console.log( `Database information:` - + `\n -> Version: ${version.number}` - + `\n -> Last version: ${lastVersion}` + + `\n -> Version: ${number}` + + `\n -> Last version: ${lastNumber}` ); - let newVersion = lastVersion ? parseInt(lastVersion) + 1 : 1; - newVersion = String(newVersion).padStart(opts.versionDigits, '0'); + let newVersion; + + if (lastNumber) + newVersion = Math.max( + parseInt(number), + parseInt(lastNumber) + ) + 1; + else + newVersion = 1; + + const versionDigits = number + ? number.length + : opts.versionDigits; + + newVersion = String(newVersion).padStart(versionDigits, '0'); // Get version name + let versionName = opts.name; + const verionsDir =`${opts.myvcDir}/versions`; + const versionNames = new Set(); const versionDirs = await fs.readdir(verionsDir); for (const versionNameDir of versionDirs) { @@ -83,11 +108,19 @@ class Version { versionDir = `${verionsDir}/${versionFolder}`; await conn.query( - `UPDATE version SET lastNumber = ? WHERE code = ?`, - [newVersion, opts.code] + `INSERT INTO version + SET code = ?, + lastNumber = ? + ON DUPLICATE KEY UPDATE + lastNumber = VALUES(lastNumber)`, + [opts.code, newVersion] ); await fs.mkdir(versionDir); - console.log(`New version folder created: ${versionFolder}`); + await fs.writeFile( + `${versionDir}/00-firstScript.sql`, + '--Place your SQL code here\n' + ); + console.log(`New version created: ${versionFolder}`); await conn.query('COMMIT'); } catch (err) { diff --git a/myvc.js b/myvc.js index 501b5a8..32ae8ce 100755 --- a/myvc.js +++ b/myvc.js @@ -9,6 +9,7 @@ const ini = require('ini'); const path = require('path'); const mysql = require('mysql2/promise'); const nodegit = require('nodegit'); +const camelToSnake = require('./lib').camelToSnake; class MyVC { async run(command) { @@ -17,12 +18,23 @@ class MyVC { `v${packageJson.version}`.magenta ); - const opts = {}; - const argv = process.argv.slice(2); - const cliOpts = getopts(argv, { + const usage = { + description: 'Utility for database versioning', + params: { + remote: 'Name of remote to use', + workspace: 'The base directory of the project', + socket: 'Wether to connect to database via socket', + debug: 'Wether to enable debug mode', + version: 'Display the version number and exit', + help: 'Display this help message' + } + }; + const baseOpts = { alias: { remote: 'r', - workspace: 'w', + workspace: 'w' + }, + boolean: { socket: 's', debug: 'd', version: 'v', @@ -31,28 +43,12 @@ class MyVC { default: { workspace: process.cwd() } - }) - - if (cliOpts.version) - process.exit(0); + }; + const opts = this.getopts(baseOpts); try { - if (!command) { - const commandName = cliOpts._[0]; - if (!commandName) { - console.log( - 'Usage:'.gray, - '[npx] myvc' - + '[-w|--workspace]' - + '[-r|--remote]' - + '[-d|--debug]' - + '[-h|--help]' - + '[-v|--version]' - + 'command'.blue - ); - process.exit(0); - } - + const commandName = opts._[0]; + if (!command && commandName) { const commands = [ 'init', 'pull', @@ -69,18 +65,54 @@ class MyVC { const Klass = require(`./myvc-${commandName}`); command = new Klass(); } - - const commandOpts = getopts(argv, command.localOpts); - Object.assign(cliOpts, commandOpts); - - for (const opt in cliOpts) - if (opt.length > 1 || opt == '_') - opts[opt] = cliOpts[opt]; - const operandToOpt = command.localOpts.operand; + if (!command) { + this.showHelp(baseOpts, usage); + process.exit(0); + } + + const commandOpts = this.getopts(command.localOpts); + Object.assign(opts, commandOpts); + + const operandToOpt = command.usage.operand; if (opts._.length >= 2 && operandToOpt) opts[operandToOpt] = opts._[1]; + if (opts.version) + process.exit(0); + + if (opts.help) { + this.showHelp(command.localOpts, command.usage, commandName); + process.exit(0); + } + + // Check version + + let depVersion; + const versionRegex = /^[^~]?([0-9]+)\.([0-9]+).([0-9]+)$/; + const wsPackageFile = path.join(opts.workspace, 'package.json'); + + if (await fs.pathExists(wsPackageFile)) { + const wsPackageJson = require(wsPackageFile); + try { + depVersion = wsPackageJson + .dependencies + .myvc.match(versionRegex); + } catch (e) {} + } + + if (depVersion) { + const myVersion = packageJson.version.match(versionRegex); + const isSameVersion = + depVersion[1] === myVersion[1] && + depVersion[2] === myVersion[2]; + + if (!isSameVersion) + throw new Error(`This version of MyVC differs from your package.json`) + } + + // Load method + parameter('Workspace:', opts.workspace); parameter('Remote:', opts.remote || 'local'); @@ -115,19 +147,23 @@ class MyVC { Object.assign(opts, config); opts.configFile = configFile; - + + if (!opts.myvcDir) + opts.myvcDir = path.join(opts.workspace, opts.subdir || ''); + // Database configuration - let iniFile = 'db.ini'; let iniDir = __dirname; + let iniFile = 'db.ini'; + if (opts.remote) { - iniFile = `remotes/${opts.remote}.ini`; - iniDir = opts.workspace; + iniDir = `${opts.myvcDir}/remotes`; + iniFile = `${opts.remote}.ini`; } const iniPath = path.join(iniDir, iniFile); if (!await fs.pathExists(iniPath)) - throw new Error(`Database config file not found: ${iniFile}`); + throw new Error(`Database config file not found: ${iniPath}`); const iniConfig = ini.parse(await fs.readFile(iniPath, 'utf8')).client; const dbConfig = { @@ -135,7 +171,6 @@ class MyVC { port: iniConfig.port, user: iniConfig.user, password: iniConfig.password, - database: opts.versionSchema, multipleStatements: true, authPlugins: { mysql_clear_password() { @@ -146,7 +181,7 @@ class MyVC { if (iniConfig.ssl_ca) { dbConfig.ssl = { - ca: await fs.readFile(`${opts.workspace}/${iniConfig.ssl_ca}`), + ca: await fs.readFile(`${opts.myvcDir}/${iniConfig.ssl_ca}`), rejectUnauthorized: iniConfig.ssl_verify_server_cert != undefined } } @@ -165,9 +200,46 @@ class MyVC { await this.conn.end(); } + getopts(opts) { + const argv = process.argv.slice(2); + const values = getopts(argv, opts); + const cleanValues = {}; + for (const opt in values) + if (opt.length > 1 || opt == '_') + cleanValues[opt] = values[opt]; + return cleanValues; + } + async dbConnect() { - if (!this.conn) - this.conn = await this.createConnection(); + const {opts} = this; + + if (!this.conn) { + const conn = this.conn = await this.createConnection(); + + const [[schema]] = await conn.query( + `SHOW DATABASES LIKE ?`, [opts.versionSchema] + ); + + if (!schema) + await conn.query(`CREATE DATABASE ??`, [opts.versionSchema]); + + const [[res]] = await conn.query( + `SELECT COUNT(*) > 0 tableExists + FROM information_schema.tables + WHERE TABLE_SCHEMA = ? + AND TABLE_NAME = 'version'`, + [opts.versionSchema] + ); + + if (!res.tableExists) { + const structure = await fs.readFile(`${__dirname}/structure.sql`, 'utf8'); + await conn.query(structure); + return null; + } + + await conn.query(`USE ??`, [opts.versionSchema]); + } + return this.conn; } @@ -176,26 +248,10 @@ class MyVC { } async fetchDbVersion() { - const {opts} = this; - - const [[res]] = await this.conn.query( - `SELECT COUNT(*) > 0 tableExists - FROM information_schema.tables - WHERE TABLE_SCHEMA = ? - AND TABLE_NAME = 'version'`, - [opts.versionSchema] - ); - - if (!res.tableExists) { - const structure = await fs.readFile(`${__dirname}/structure.sql`, 'utf8'); - await this.conn.query(structure); - return null; - } - const [[version]] = await this.conn.query( `SELECT number, gitCommit FROM version WHERE code = ?`, - [opts.code] + [this.opts.code] ); return version; } @@ -278,7 +334,7 @@ class MyVC { } async cachedChanges() { - const dumpDir = `${this.opts.workspace}/dump`; + const dumpDir = `${this.opts.myvcDir}/dump`; const dumpChanges = `${dumpDir}/.changes`; if (!await fs.pathExists(dumpChanges)) @@ -300,6 +356,40 @@ class MyVC { } return changes; } + + showHelp(opts, usage, command) { + const prefix = `${'Usage:'.gray} [npx] myvc`; + + if (command) { + let log = [prefix, command.blue]; + if (usage.operand) log.push(`[<${usage.operand}>]`); + if (opts) log.push('[]'); + console.log(log.join(' ')) + } else + console.log(`${prefix} [] ${''.blue} []`); + + if (usage.description) + console.log(`${'Description:'.gray} ${usage.description}`); + + if (opts) { + console.log('Options:'.gray); + this.printOpts(opts, usage, 'alias'); + this.printOpts(opts, usage, 'boolean'); + this.printOpts(opts, usage, 'string'); + } + } + + printOpts(opts, usage, group) { + const optGroup = opts[group]; + if (optGroup) + for (const opt in optGroup) { + const paramDescription = usage.params[opt] || ''; + let longOpt = opt; + if (group !== 'boolean') longOpt += ` `; + longOpt = camelToSnake(longOpt).padEnd(22, ' ') + console.log(` -${optGroup[opt]}, --${longOpt} ${paramDescription}`); + } + } } module.exports = MyVC; diff --git a/package.json b/package.json index c257af2..5954840 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "myvc", - "version": "1.1.13", + "version": "1.2.1", "author": "Verdnatura Levante SL", "description": "MySQL Version Control", "license": "GPL-3.0", diff --git a/template/versions/00001-firstVersion/00-firstScript.sql b/template/versions/00001-firstVersion/00-firstScript.sql new file mode 100644 index 0000000..371c2c3 --- /dev/null +++ b/template/versions/00001-firstVersion/00-firstScript.sql @@ -0,0 +1 @@ +-- Place your SQL code here diff --git a/template/versions/00001-firstVersion/00-test.sql b/template/versions/00001-firstVersion/00-test.sql deleted file mode 100644 index 29daecb..0000000 --- a/template/versions/00001-firstVersion/00-test.sql +++ /dev/null @@ -1 +0,0 @@ -SET @test = NULL;