Skip to content

Instantly share code, notes, and snippets.

@ottomata
Created June 12, 2019 21:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ottomata/0470af3713c315537040ba30902794ae to your computer and use it in GitHub Desktop.
Save ottomata/0470af3713c315537040ba30902794ae to your computer and use it in GitHub Desktop.
#!/usr/bin/env node
'use strict';
const util = require('util');
const _ = require('lodash');
const yaml = require('js-yaml');
const path = require('path');
const glob = require('glob');
const semver = require('semver');
const NodeGit = require('nodegit');
const fs = require("fs");
const writeFile = util.promisify(fs.writeFile);
const unlink = util.promisify(fs.unlink);
const symlink = util.promisify(fs.symlink);
/**
* Converts a utf-8 byte buffer or a YAML/JSON string into
* an object and returns it.
* @param {string|Buffer|Object} data
* @return {Object}
*/
function objectFactory(data) {
// If we were given a byte Buffer, parse it as utf-8 string.
if (data instanceof Buffer) {
data = data.toString('utf-8');
} else if (_.isObject(data)) {
// if we were given a a JS object, return it now.
return data;
}
// If we now have a string, then assume it is a YAML/JSON string.
if (_.isString(data)) {
data = yaml.safeLoad(data);
} else {
throw new Error(
'Could not convert data into an object. ' +
'Data must be a utf-8 byte buffer or a YAML/JSON string'
);
}
return data;
}
function writeYamlFile(object, outputPath) {
return writeFile(outputPath, yaml.dump(object));
}
const defaultOptions = {
gitReference: 'HEAD',
gitReferenceType: NodeGit.Reference.TYPE.SYMBOLIC, // NodeGit.Reference.TYPE.
shouldSymlink: true,
// TODO add option to generate and expect JSON rather than yaml
contentType: 'yaml',
schemaVersionRegex: /.*\/(\d+\.\d+\.\d+).yaml$/
};
class EventSchema {
constructor(
schemaDirectory,
repository,
options = {}
) {
_.defaults(this, options, defaultOptions);
this.schemaDirectory = schemaDirectory;
this.repository = repository;
// this.schemaVersionRegex = new RegExp(`(\d+\.\d+\.\d+)\.${this.contentType}$`);
}
static async init(schemaDirectory) {
const absoluteDir = path.resolve(schemaDirectory);
const repo = await NodeGit.Repository.open(
await NodeGit.Repository.discover(absoluteDir, 0, '/')
);
return new EventSchema(schemaDirectory, repo);
}
// TODO: Can we create a function that inspects the diff of the schemas
// auto generates a semver.inc 'release' argument value?
// E.g. if only descriptions change, this can be 'patch'.
// If fields added, this can be 'minor'.
// If field types changed, this can be 'major'.
// static releaseTypeOfChange(origSchema, newSchema) {
// }
extractVersion(schemaPath) {
const match = schemaPath.match(this.schemaVersionRegex);
if (match) {
return match[1];
} else {
return null;
}
}
// TODO: Is this useful? We could just always assume we should work with HEAD via repo.getHeadCommit()
async getCommit() {
if (this.gitReferenceType == NodeGit.Reference.TYPE.SYMBOLIC) {
// If this is a valid name, it is something like HEAD or refs/heads/branchname
if (NodeGit.Reference.isValidName(this.gitReference)) {
const oid = await NodeGit.Reference.nameToId(this.repository, this.gitReference);
return this.repository.getCommit(oid);
} else {
// assume this is a branch
return this.repository.getBranchCommit(this.gitReference);
}
} else if (this.gitReferenceType == NodeGit.Reference.TYPE.OID) {
// Else this is a sha commit id.
return this.repository.getCommit(this.gitReference);
} else {
throw new Error(`invalid gitReferenceType ${this.gitReferenceType}`);
}
}
async schemaDirectoryEntries() {
const commit = await this.getCommit();
const tree = await NodeGit.Tree.lookup(this.repository, commit.treeId());
const schemaDirectoryTreeEntry = await tree.getEntry(this.schemaDirectory);
if (!schemaDirectoryTreeEntry.isTree()) {
throw new Error(`${this.schemaDirectory} is not a git tree!`);
}
const schemaDirectoryTree = await schemaDirectoryTreeEntry.getTree();
return schemaDirectoryTree.entries();
}
async entryContent(entry) {
const blob = await entry.getBlob();
return blob.content();
}
async schemaEntries() {
const directoryEntries = await this.schemaDirectoryEntries();
const schemaEntries = _.filter(directoryEntries, (entry) => {
return entry.isFile() && entry.path().match(this.schemaVersionRegex);
});
return _.sortBy(schemaEntries, (entry) => entry.path());
}
async versions() {
const schemaEntries = await this.schemaEntries();
return schemaEntries.map(entry => this.extractVersion(entry.path()));
}
async latestVersion() {
const versions = await this.versions();
return _.last(versions);
}
async versionEntry(version) {
return _.find(await this.schemaEntries(), (entry) => {
return version == this.extractVersion(entry.path());
})
}
async versionContent(version) {
return this.entryContent(await this.versionEntry(version));
}
async versionObject(version) {
return objectFactory(await this.versionContent(version));
}
async latestVersionContent() {
return this.versionContent(await this.latestVersion());
}
async latestVersionObject() {
return this.versionObject(await this.latestVersion());
}
async needsNewVersion(candidateSchema) {
const latestSchema = await this.latestVersionObject();
return !_.isEqual(latestSchema, candidateSchema);
}
async nextVersion(release='minor') {
const latestVersion = await this.latestVersion();
return semver.inc(latestVersion, release);
}
async nextVersionPath() {
const nextVersion = await this.nextVersion();
return `${this.schemaDirectory}/${nextVersion}.${this.contentType}`;
}
// createExtensionlessSymlink(schemaPath) {
// const extensionlessPath = path.join(this.schemaDirectory, path.basename(schemaPath, `.${this.contentType}`))
// const destPath = path.basename(schemaPath);
// return symlink(path.basename(schemaPath), extensionlessPath);
// }
async generateNextVersion(candidateSchema) {
const nextVersionPath = await this.nextVersionPath();
// TODO: dereference schema $refs.
await writeYamlFile(candidateSchema, nextVersionPath);
return nextVersionPath;
// if (this.shouldSymlink) {
// this.createExtensionlessSymlink(nextVersionPath);
// }
// TODO: I want to git add this new file right here, but
// I can't because the repo index is locked while this filter clean process runs.
// WIP...
// const lockedIndex = await NodeGit.Index.open("index.lock");
// lockedIndex.write();
// const index = await this.repository.refreshIndex();
// let relock = false;
// if (fs.existsSync('.git/index.lock')) {
// console.error('index.lock exists, removing');
// await unlink('.git/index.lock');
// relock = true;
// }
// const index = await this.repository.refreshIndex();
// console.error(`adding ${nextVersionPath} to ${index.path()}`);
// await index.addByPath(nextVersionPath);
// await index.write();
// if (relock) {
// console.error("relocking");
// fs.closeSync(fs.openSync('.git/index.lock', 'w'));
// }
}
toString() {
return `Schema ${this.schemaDirectory}`;
}
}
if (require.main === module) {
const schemaPath = process.argv[2];
const data = fs.readFileSync(0, 'utf-8');
const candidateSchema = objectFactory(data);
EventSchema.init(path.dirname(schemaPath)).then(async (es) => {
const needsNewVersion = await es.needsNewVersion(candidateSchema);
if (needsNewVersion) {
console.error(`${es} needs new version: ${await es.nextVersion()}`);
try {
const newVersionPath = await es.generateNextVersion(candidateSchema);
console.error(`Generated new schema version for ${es}. Before committing, please run: git add ${newVersionPath}`);
} catch(err) {
console.error("FAILED ", err);
}
} else {
console.error(`${es} does not need new version. Latest is ${await es.latestVersion()}`);
}
});
}
module.exports = {
EventSchema,
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment