Skip to content

Instantly share code, notes, and snippets.

@maruf89
Last active February 8, 2021 07:21
Show Gist options
  • Save maruf89/419b4a929f9d133d3425ebfd482a79e9 to your computer and use it in GitHub Desktop.
Save maruf89/419b4a929f9d133d3425ebfd482a79e9 to your computer and use it in GitHub Desktop.
Node update version in file git precommit hook – checks if certain staged files changed, and updates the version number of a correllating file, then git adds that file to be committed as well
#!/usr/bin/env node
// This precommit checks if certain staged files changed, and updates the version number of a correllating file, then `git add`s that file to be committed as well
// Useful for breaking caches like in WordPress which will serve the same CSS or JS if you forget to increment the version
//
// Example test usage after you added your program and regex match at the end of the `defaultArgs` (line 26)
// `git add assets/dist/community-directory.js` // A file correlating to `programs` (line 46)
// `node .git/hooks/precommit -s=true` // Will simulate a change
// `node .git/hooks/precommit -t=major` // Updates the version, increments the major value '1.5.5' => '1.6.0'
const fs = require('fs');
const readline = require('readline');
const myArgs = process.argv.slice(2);
var path = require('path');
const childProcess = require('child_process');
const exec = childProcess.exec;
const execSync = childProcess.execSync;
// Get's a list of staged files
const commitedFilesCommand = 'git diff --name-only --cached';
// Assuming this file is located in rootDirectory/.git/hooks
var appDir = path.resolve(path.dirname(require.main.filename), '../..');
const defaultArgs = {
type: 'minor', // Options are `year`, `major`, and `minor`
t: 'type',
delimiter: '.',
d: 'delimiter',
simulate: false, // call true to simulate what the change would look like
s: 'simulate',
// This is the program - To set your own replace 'plugin' or add new ones like 'cssPath + cssPattern'
pluginPath: 'community-directory.php',// The file which has the version we want to update
// a regex that matches the line with the version, the current version as a whole, and each individual version number, assuming there's three numbers
// Example string: " define( 'COMMUNITY_DIRECTORY_VERSION', '0.6.3' );" regex match => {"COMMUNITY_DIRECTORY_VERSION', '0.6.3", "0.6.3", "0", "6", "3"}
pluginPattern: /COMMUNITY_DIRECTORY_VERSION.+?(?:'|")((\d{1,4})\.(\d{1,2})\.(\d{1,2}))/,
};
const parsedArgs = myArgs.reduce((acc, arg) => {
let [key, val] = arg.split('=');
if (/--/.test(key)) acc[key.substr(2)] = val;
else if (/-/.test(key)) acc[acc[key.substr(1)]] = val;
return acc;
}, { ...defaultArgs });
// If any of these files change, run the program defined as the value
const programs = {
'assets/dist/community-directory.js': 'plugin',
'assets/dist/community-directory.css': 'plugin',
}
// Command to add the newly version-updated file
const gitAdd = file => execSync(`git add ${file}`);
exec(commitedFilesCommand, (err, stdout) => {
const files = stdout.split(/\n/g);
// Filter the programs so they're distinct
const promises = files.reduce((acc, watchFile) => {
const program = programs[watchFile];
if (program && !acc.includes(program)) acc.push(program);
return acc;
}, [])
// Then map them to promises
.map(program => {
const file = path.resolve(appDir, parsedArgs[`${program}Path`]);
const pattern = parsedArgs[`${program}Pattern`];
console.log(`CSS or JS file has changed, updating ${program} version in ${file}`);
if (!file) return Promise.reject(`Invalid program: ${program} and path ${program}Path`);
return getNewVersion(file, pattern).then(replaceLine.bind(null, file)).then((...args) => {
console.log(args);
gitAdd(file);
console.log(`ran 'git add ${file}'`);
}).catch(err => {
console.log('Error occurred');
console.error(err);
});
});
Promise.all(promises).then(() => {
process.exit(0);
})
});
function readInterface(path) {
return readline.createInterface({
input: fs.createReadStream(path),
crlfDelay: Infinity,
});
}
function getNewVersion(file, pattern) {
return new Promise((resolve, reject) => {
const interface = readInterface(file);
let lineNum = 0;
interface.on('line', line => {
++lineNum;
if (pattern.test(line)) {
const method = methods[`on_${parsedArgs.type}`];
if (!method) {
console.error('Invalid type called, must be one of: `year`, `major`, or `minor`');
reject();
}
// Slice off the first part that matched everything, which isn't needed
const parts = line.match(pattern).slice(1);
// Get the old version '0.6.3'
const oldV = parts.shift();
// Build the new version => '0.6.4'
let newV = method(...parts).join(parsedArgs.delimiter);
// Replace the version
const newVersion = line.replace(oldV, newV);
// Stop reading the file
interface.close();
resolve([newVersion, line]);
}
});
});
}
function replaceLine(file, [newLine, oldLine]) {
return new Promise((resolve, reject) => {
fs.readFile(file, 'utf8', (err, data) => {
if (err) return reject(err);
var result = data.replace(oldLine, newLine);
if (!parsedArgs.simulate) {
fs.writeFile(file, result, 'utf8', function (err) {
if (err) return reject(err)
resolve(`Changed from: ${oldLine} => ${newLine}`);
});
} else
resolve(`Simulating change from: ${oldLine} => ${newLine}`);
});
});
}
const methods = {
pad: (num, size) => {
if (String(num).length > size) size = String(num).length;
var s = "000000000" + num;
return s.substr(s.length-size);
},
increment: stringNum => methods.pad(+stringNum + 1, stringNum.length),
on_year: (year) => [methods.increment(year), '0', '0'],
on_major: (year, major) => [year, methods.increment(major), '0'] ,
on_minor: (year, major, minor) => [year, major, methods.increment(minor)],
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment