javascriptgitnpmhuskygit-husky

Can you git add new files during the Husky "commit-msg" hook?


I am trying to setup an auto-versioning system where if I git commit with the message PATCH: {message}, the app's patch version will automatically update (and likewise for a prefix of MINOR and MAJOR as well). I am using Husky for my pre-commit and pre-push hook, so I am trying to get this working with a .husky/commit-msg hook that looks like this:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run auto-version $1 && git add --all

This works as desired with my auto-version.js script automatically reading the commit message and updating ./version.json accordingly. The only problem is the commit is created with the old version.json file and I'm unsure why. I can tell the git add is functional because I am left with the updated version.json file sitting in the Staged Changes section after committing. My .husky/pre-commit hook looks like such and stages updated files prior to committing just fine:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint && npm run format && git add --all

I think this could be related to the timing between when the commit-msg hook is fired and when git will accept newly staged files, but Husky doesn't provide great docs on how the commit-msg hook functions. I also tried setting this up using the pre-commit hook instead, but the new commit message does not get saved to .git/COMMIT_EDITMSG at this stage (only the old commit message is present).

For some additional context, we are currently just firing npm version patch --no-git-tag-version on the pre-commit hook and then manually changing the minor and major versions when needed. I wanted to make a more robust and automatic system, which lead to this blocker.

auto-version.js
const { readFileSync, writeFileSync } = require('fs');

const versionJsonPath = './version.json';
const commitMessagePath = '.git/COMMIT_EDITMSG';

const prefixOptions = ['PATCH', 'MINOR', 'MAJOR'];
const postfixMinLength = 12;

(() => {
    // read commit message
    const rawMessage = readFileSync(commitMessagePath, 'utf-8');
    const message = rawMessage.split(/\r?\n/)[0];
    console.log(`Reading commit message "${message}"...`);

    // check for merge commit
    if (message.startsWith('Merge branch')) {
        process.exit();
    }

    // check for core composition
    const messageParts = message.split(':');
    if (messageParts.length != 2) {
        throwError(`Commit message should take the form "{${prefixOptions.join('|')}}: {message}".`);
    }

    // check for valid prefix
    const messagePrefix = messageParts[0];
    if (!prefixOptions.includes(messagePrefix)) {
        throwError(`Commit message prefix must be one of the following version types: [${prefixOptions.join(', ')}].`);
    }

    // check for valid postfix
    const messagePostfix = messageParts[1];
    if (messagePostfix.trim().length < postfixMinLength) {
        throwError(`Commit message postfix must be at least ${postfixMinLength} characters.`);
    }

    // update app version
    const versionJson = JSON.parse(readFileSync(versionJsonPath, 'utf-8'));
    const oldVersion = versionJson.appVersion;
    const versionParts = oldVersion.split('.').map(v => parseInt(v, 10));

    if (messagePrefix == 'MAJOR') {
        versionParts[0]++;
        versionParts[1] = 0;
        versionParts[2] = 0;
    } else if (messagePrefix == 'MINOR') {
        versionParts[1]++;
        versionParts[2] = 0;
    } else {
        versionParts[2]++;
    }

    const newVersion = versionParts.join('.');
    versionJson.appVersion = newVersion;

    console.log(`Updating app version from ${oldVersion} to ${newVersion}...`);
    writeFileSync(versionJsonPath, JSON.stringify(versionJson));

    process.exit();
})();

function throwError(message) {
    console.error(message);
    process.exit(1);
}
version.json
{
    "appVersion": "0.12.15"
}

Solution

  • Looking at another post (Git hook commit-msg git add file), it seems that doing git add is not possible during/after the commit-msg hook stage. Based on one of the answers there, I solved the problem by using a post-commit hook to amend the commit to include the updated version.json file with git commit --amend -C HEAD -n version.json.

    .husky/post-commit
    #!/usr/bin/env sh
    . "$(dirname -- "$0")/_/husky.sh"
    
    npm run auto-version-post-commit
    

    The post-commit script ensures there is a version.json file to add to prevent infinite git hook recursion:

    var exec = require('child_process').exec;
    
    const versionJsonPath = 'version.json';
    
    (() => {
        exec('git diff --name-only', (error, stdout, stderr) => {
            const modifiedFiles = stdout.trim().split(/\r?\n/);
    
            // check if version.json has been modified by commit-msg hook
            if (modifiedFiles.includes(versionJsonPath)) {
                // amend the last commit to include the updated version.json
                exec(`git commit --amend -C HEAD -n ${versionJsonPath}`);
            }
        });
    })();