diff --git a/README.md b/README.md index 3ef4943..61b3499 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ From now on, you can use the project as if it were a standard git repository desired remote. ```text -$ myvc push [] +$ myvc push [] [--save-commit] ``` ### Routines @@ -164,10 +164,10 @@ $ myvc init Incorporates database routine changes into workspace. ```text -$ myvc pull [remote] [-f|--force] [-c|--checkout] [-u|--update] +$ myvc pull [remote] [-f|--force] [-c|--checkout] [-u|--update] [-s|save-sums] ``` -When *checkout* option is provided, it does the following before export: +When *--checkout* option is provided, it does the following before export: 1. Get the last database push commit (saved in versioning tables). 2. Creates and checkout to a new branch based in database commit. @@ -177,16 +177,21 @@ When *checkout* option is provided, it does the following before export: Applies versions and routine changes into database. ```text -$ myvc push [] [-f|--force] +$ myvc push [] [-f|--force] [-c|--save-commit] [-s|save-sums] ``` +Commit is saved into database only if *--save-commit* option is provided, it +prevents from accidentally saving local commits into shared servers, causing +subsequent pushes from other clients to fail because they can't get that +commit from the git tree in order to get differences. + ### version Creates a new version folder, when name is not specified it generates a random name mixing a color with a plant name. ```text -$ myvc version [] +$ myvc version [] [-c|--no-clean] ``` ## Local server commands diff --git a/lib.js b/lib.js index c4a7009..553b8f3 100644 --- a/lib.js +++ b/lib.js @@ -24,7 +24,7 @@ class Exporter { this.attrs = require(`${templateDir}.js`); } - async export(exportDir, schema, newSums, oldSums, update) { + async export(exportDir, schema, update, saveSum) { const res = await this.query(schema); if (!res.length) return; @@ -45,15 +45,32 @@ class Exporter { await fs.remove(`${routineDir}/${routine}.sql`); } + const engine = this.engine; + for (const params of res) { const routineName = params.name; const sql = this.format(params); const routineFile = `${routineDir}/${routineName}.sql`; - const shaSum = this.engine.shaSum(sql); - newSums[routineName] = shaSum; + const oldSum = engine.getShaSum(routineName); + if (oldSum || saveSum) { + const shaSum = engine.shaSum(sql); + if (oldSum !== shaSum) { + engine.setShaSum( + this.objectType, schema, routineName, shaSum); + update = true; + } + } else if (params.modified && engine.lastPull) { + if (params.modified > engine.lastPull) + update = true; + } else if (await fs.pathExists(routineFile)) { + const currentSql = await fs.readFile(routineFile, 'utf8'); + if (sql != currentSql) + update = true; + } else + update = true; - if (oldSums[routineName] !== shaSum || update) + if (update) await fs.writeFile(routineFile, sql); } } @@ -104,16 +121,26 @@ class Exporter { class ExporterEngine { constructor(conn, myvcDir) { this.conn = conn; - this.shaFile = `${myvcDir}/.shasums.json`; + this.pullFile = `${myvcDir}/.pullinfo.json`; this.exporters = []; this.exporterMap = {}; } async init () { - if (await fs.pathExists(this.shaFile)) - this.shaSums = JSON.parse(await fs.readFile(this.shaFile, 'utf8')); - else - this.shaSums = {}; + if (await fs.pathExists(this.pullFile)) { + this.pullInfo = JSON.parse(await fs.readFile(this.pullFile, 'utf8')); + const lastPull = this.pullInfo.lastPull; + if (lastPull) + this.pullInfo.lastPull = new Date(lastPull); + } else + this.pullInfo = { + lastPull: null, + shaSums: {} + }; + + this.shaSums = this.pullInfo.shaSums; + this.lastPull = this.pullInfo.lastPull; + this.infoChanged = false; const types = [ 'function', @@ -150,6 +177,14 @@ class ExporterEngine { .digest('hex'); } + getShaSum(type, schema, name) { + try { + return this.shaSums[schema][type][name]; + } catch (e) {}; + + return null; + } + setShaSum(type, schema, name, shaSum) { if (!shaSum) { this.deleteShaSum(type, schema, name); @@ -162,17 +197,32 @@ class ExporterEngine { if (!shaSums[schema][type]) shaSums[schema][type] = {}; shaSums[schema][type][name] = shaSum; + this.infoChanged = true; } deleteShaSum(type, schema, name) { try { delete this.shaSums[schema][type][name]; + this.infoChanged = true; } catch (e) {}; } - async saveShaSums() { - await fs.writeFile(this.shaFile, - JSON.stringify(this.shaSums, null, ' ')); + deleteSchemaSums(schema) { + delete this.shaSums[schema]; + this.infoChanged = true; + } + + async refreshPullDate() { + const [[row]] = await this.conn.query(`SELECT NOW() now`); + this.pullInfo.lastPull = row.now; + this.infoChanged = true; + } + + async saveInfo() { + if (!this.infoChanged) return; + await fs.writeFile(this.pullFile, + JSON.stringify(this.pullInfo, null, ' ')); + this.infoChanged = false; } } diff --git a/myvc-pull.js b/myvc-pull.js index 093f798..9d556b2 100755 --- a/myvc-pull.js +++ b/myvc-pull.js @@ -10,7 +10,8 @@ class Pull { params: { force: 'Do it even if there are local changes', checkout: 'Move to same database commit before pull', - update: 'Update routine file even is shasum is the same' + updateAll: 'Update all routines', + saveSums: 'Save SHA sums of all objects' }, operand: 'remote' }; @@ -21,12 +22,14 @@ class Pull { alias: { force: 'f', checkout: 'c', - update: 'u' + updateAll: 'u', + saveSums: 's' }, boolean: [ 'force', 'checkout', - 'update' + 'updateAll', + 'saveSums' ] }; } @@ -102,7 +105,7 @@ class Pull { for (const schema in shaSums) { if (!await fs.pathExists(`${exportDir}/${schema}`)) - delete shaSums[schema]; + engine.deleteSchemaSums(schema); } // Export objects to SQL files @@ -111,19 +114,14 @@ class Pull { let schemaDir = `${exportDir}/${schema}`; if (!await fs.pathExists(schemaDir)) await fs.mkdir(schemaDir); - if (!shaSums[schema]) - shaSums[schema] = {}; - const sums = shaSums[schema]; - for (const exporter of engine.exporters) { - const type = exporter.objectType; - const oldSums = sums[type] || {}; - sums[type] = {}; - await exporter.export(exportDir, schema, sums[type], oldSums, opts.update); - } + for (const exporter of engine.exporters) + await exporter.export(exportDir, + schema, opts.update, opts.saveSums); } - await engine.saveShaSums(); + await engine.refreshPullDate(); + await engine.saveInfo(); } } diff --git a/myvc-push.js b/myvc-push.js index ca61a41..6b8a718 100644 --- a/myvc-push.js +++ b/myvc-push.js @@ -12,7 +12,9 @@ class Push { return { description: 'Apply changes into database', params: { - force: 'Answer yes to all questions' + force: 'Answer yes to all questions', + saveCommit: 'Wether to save the commit SHA into database', + saveSums: 'Save SHA sums of pushed objects' }, operand: 'remote' }; @@ -21,10 +23,14 @@ class Push { get localOpts() { return { alias: { - force: 'f' + force: 'f', + saveCommit: 'c', + saveSums: 's' }, boolean: [ - 'force' + 'force', + 'saveCommit', + 'saveSums' ] }; } @@ -33,6 +39,9 @@ class Push { const conn = await myvc.dbConnect(); this.conn = conn; + if (opts.saveCommit == null && opts.remote == 'local') + opts.saveCommit = true; + // Obtain exclusive lock const [[row]] = await conn.query( @@ -142,16 +151,16 @@ class Push { if (versionDir == 'README.md') continue; - const match = versionDir.match(/^([0-9]+)-([a-zA-Z0-9]+)?$/); - if (!match) { + const dirVersion = myvc.parseVersionDir(versionDir); + if (!dirVersion) { logVersion('[?????]'.yellow, versionDir, `Wrong directory name.` ); continue; } - const versionNumber = match[1]; - const versionName = match[2]; + const versionNumber = dirVersion.number; + const versionName = dirVersion.name; if (versionNumber.length != version.number.length) { logVersion('[*****]'.gray, versionDir, @@ -283,7 +292,7 @@ class Push { await engine.init(); async function finalize() { - await engine.saveShaSums(); + await engine.saveInfo(); if (routines.length) { await conn.query('FLUSH PRIVILEGES'); @@ -303,6 +312,7 @@ class Push { if (exists) newSql = await fs.readFile(fullPath, 'utf8'); const oldSql = await engine.fetchRoutine(type, schema, name); + const oldSum = engine.getShaSum(type, schema, name); const isEqual = newSql == oldSql; let actionMsg; @@ -336,7 +346,8 @@ class Push { ); } - await engine.fetchShaSum(type, schema, name); + if (opts.saveSums || oldSum) + await engine.fetchShaSum(type, schema, name); } else { const escapedName = scapedSchema + '.' + @@ -366,7 +377,7 @@ class Push { } else console.log(` -> No routines changed.`); - if (gitExists) { + if (gitExists && opts.saveCommit) { const repo = await nodegit.Repository.open(this.opts.workspace); const head = await repo.getHeadCommit(); diff --git a/myvc-version.js b/myvc-version.js index 311cd79..f150c98 100644 --- a/myvc-version.js +++ b/myvc-version.js @@ -10,7 +10,8 @@ class Version { return { description: 'Creates a new version', params: { - name: 'Name for the new version' + name: 'Name for the new version', + noClean: 'Do not clean old versions' }, operand: 'name' }; @@ -19,11 +20,15 @@ class Version { get localOpts() { return { alias: { - name: 'n' + name: 'n', + noClean: 'c' }, string: [ 'name' ], + boolean: [ + 'noClean' + ], default: { remote: 'production' } @@ -31,7 +36,9 @@ class Version { } async run(myvc, opts) { - let versionDir; + let newVersionDir; + const verionsDir =`${opts.myvcDir}/versions`; + const oldVersions = []; // Fetch last version number @@ -74,14 +81,16 @@ class Version { // 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) { - const split = versionNameDir.split('-'); - const versionName = split[1]; - if (versionName) versionNames.add(versionName); + for (const versionDir of versionDirs) { + const dirVersion = myvc.parseVersionDir(versionDir); + if (!dirVersion) continue; + versionNames.add(dirVersion.name); + + if (parseInt(dirVersion.number) < parseInt(number)) + oldVersions.push(versionDir); } if (!versionName) { @@ -107,7 +116,7 @@ class Version { // Create version const versionFolder = `${newVersion}-${versionName}`; - versionDir = `${verionsDir}/${versionFolder}`; + newVersionDir = `${verionsDir}/${versionFolder}`; await conn.query( `INSERT INTO version @@ -117,9 +126,9 @@ class Version { lastNumber = VALUES(lastNumber)`, [opts.code, newVersion] ); - await fs.mkdir(versionDir); + await fs.mkdir(newVersionDir); await fs.writeFile( - `${versionDir}/00-firstScript.sql`, + `${newVersionDir}/00-firstScript.sql`, '-- Place your SQL code here\n' ); console.log(`New version created: ${versionFolder}`); @@ -127,10 +136,23 @@ class Version { await conn.query('COMMIT'); } catch (err) { await conn.query('ROLLBACK'); - if (versionDir && await fs.pathExists(versionDir)) - await fs.remove(versionDir, {recursive: true}); + if (newVersionDir && await fs.pathExists(newVersionDir)) + await fs.remove(newVersionDir, {recursive: true}); throw err; } + + // Remove old versions + + if (opts.maxOldVersions && !opts.noClean + && oldVersions.length > opts.maxOldVersions) { + oldVersions.splice(-opts.maxOldVersions); + + for (const oldVersion of oldVersions) + await fs.remove(`${verionsDir}/${oldVersion}`, + {recursive: true}); + + console.log(`Old versions deleted: ${oldVersions.length}`); + } } } diff --git a/myvc.default.yml b/myvc.default.yml index 505d473..6b7d68f 100755 --- a/myvc.default.yml +++ b/myvc.default.yml @@ -1,5 +1,6 @@ versionSchema: myvc versionDigits: 5 +maxOldVersions: 30 schemas: - myvc fixtures: diff --git a/myvc.js b/myvc.js index fb59e33..430be5e 100755 --- a/myvc.js +++ b/myvc.js @@ -33,7 +33,7 @@ class MyVC { alias: { remote: 'r', workspace: 'w', - socket: 's', + socket: 'k', debug: 'd', version: 'v', help: 'h' @@ -275,6 +275,15 @@ class MyVC { return version; } + parseVersionDir(versionDir) { + const match = versionDir.match(/^([0-9]+)-([a-zA-Z0-9]+)?$/); + if (!match) return null; + return { + number: match[1], + name: match[2] + }; + } + async changedRoutines(commitSha) { const repo = await this.openRepo(); const changes = []; @@ -302,7 +311,20 @@ class MyVC { const head = await repo.getHeadCommit(); if (head && commitSha) { - const commit = await repo.getCommit(commitSha); + let commit; + try { + await repo.fetchAll(); + } catch(err) { + console.warn(err.message.yellow); + } + try { + commit = await repo.getCommit(commitSha); + } catch (err) { + if (err.errorFunction == 'Commit.lookup') + throw new Error(`Commit id (${commitSha}) not found, you may have to run 'git fetch' first`); + else + throw err; + } const commitTree = await commit.getTree(); const headTree = await head.getTree(); diff --git a/package.json b/package.json index 7adcb5e..605390a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "myvc", - "version": "1.3.13", + "version": "1.4.0", "author": "Verdnatura Levante SL", "description": "MySQL Version Control", "license": "GPL-3.0", diff --git a/server/Dockerfile b/server/Dockerfile index 38706b7..43578a3 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -26,7 +26,6 @@ WORKDIR /myvc COPY \ package.json \ - package-lock.json \ ./ RUN npm install --only=prod