KEMBAR78
Using javaScript in your git hooks | by Sergeon | Medium
Sitemap

Using javaScript in your git hooks

8 min readMay 7, 2018

Git hooks really come in handy to automatically perform a lot of repetitive tasks when working with git, and also can be used to ensure that some requirements are met before you can commit or push in a client repository, or before a remote repository can accept new commits. For instance, is really common to execute git hooks to check wether a test suite ran successfully, and with a certain coverage threshold.

Now, to do so, you just need to edit your hook scripts, which lie within the .git/hooks/ directory in any git repo. Such scripts are run before or after certain events, and git will use their output to make some changes (i.e., append a signature automatically to every commit message) or to prevent things for happening (for instance, prevent you to make a push if the linter output has errors or warnings).

By default, in any git repository, this folder is populated with a lot of sample hook scripts, that have names like pre-push.sample. If you inspect such sample files, you will see that they are just shell scripts that usually exit with 1 or 0 based on conditions. Sometimes, they print something to get the job done: for instance, in the prepare-commit-msg hook you need to print to stdout in order to indicate the desired commit message.

Exit with an 1 code will prevent the related action for happening: if you exit with 1 from the pre-commit hook, the commit will be rejected. On the other hand, 0 means that everything is ok. This is the standard in the shell scripts world.

As you would see if you inspected the default hooks, they are all written in bash. And one of the best sources of documentation for git, the pro git book, uses ruby as the scripting language for its examples.

Using javaScript

Now, you can use any scripting language for your shell scripts or git hooks, would it be python, php, javaScript or any other, you just need to know some little things and adhere to certain criteria.

(this article really will help you a lot if you never used javaScript as a cli language).

In order to use javaScript code to be run in the shell, we will use node.js. In any shell script, to tell your OS that the script is intended to be run by node, you just need to add this line to the first line of the script:

#!/usr/bin/env node

Now, if you put this in any file, toss some javaScript at it, and execute it, you should see the output in your terminal. For instance, let’s say we create this file:

Then we make it executable:

sudo chmod 774 ./test.js

Now, if we just run the file:

./test.js

We will see the ‘Hello World’ output in terminal.

Now notice how this is different from executing test.js with node. The command:

node ./test.js

would have the same output. However, by appending the #!/usr/bin/env node to the first line, we’re telling the OS that it should use node to execute the file by default: there is no need to invoke node explicitly. This is important in the git hook context because git hooks are just executed as a shell script: the OS will use node to do so only if we explicitly mark the file to be executed by node.

Stdout and exit codes

As we just see, console.log just writes to the standard output, so it works like print in a bash script. Should we need to output some specific message in a git hook, we just can use the old good console.log to get the job done.

Now, how to exit in a node script? Well, if you never done cli node.js before, you may think that return will do the trick. However, that won’t work. In order to send an exit code from a node shell script, we must use the process module. The process.exit() method will just work like a bash exit statement.

So, for our use case, we will just process.exit(1) whenever we want to abort some git event, or process.exit(0) when things are OK.

Checking commit messages

Let’s say we want to check if a commit message adheres to a certain structure. For instance, in my job, we use Jira to manage our issues, and Jira issue tickets have an id like: PN-123, where PN are just the initials of the project name. So, when we create a branch or add a commit to a repository, they should reference a Jira issue: branches usually reference user stories or bugs, and commits usually reference tasks or subtasks.

So, we prepend the special feature/ or hotfix/ string and an user story ID in every branch we create, like:

feature/PN-301-allow-user-logout

And a plain ID in every commit:

PN-440-added-generic-error-notifications-styles

With most repository managers, like github, gitlab or bitbucket, is easy to enforce such policies just from the repository manager UI. So, for instance, we got our remote repositories to reject commits that don’t adhere to this naming policy.

Now, the problem is that this doesn’t prevent developers to mistakenly create wrong branch names or commit messages in their local repositories. And when they will push their work, it will be rejected, and they will be forced to rewrite history and do some git nasty tricks to fix those mistakes.

So, one good thing we could do in such a scenario is to setup a git hook to prevent bad commit messages to be committed directly in our local repository: now, whenever we make such a mistake, we can fix it at the right time.

When we want to check the commit message, the right hook is the commit-msg hook. We should add a commit-msg file to the hooks directory. In the hook, we will perform the following tasks:

  • Check wether the branch name is correct. If not, we will print an useful error message end exit with 1.
  • Check wether the commit message is correct. If not, we will print an useful error message and exit with 1.
  • If everything is ok, we exit with 0.

To check the branch name in every commit is a bit redundant, but as far as I know there is not a dedicated hook that runs when a new branch is created. Checking both the branch name and the commit message will ensure that the problem will be noticed whenever we try to add the first commit to a bad named branch, allowing us to rename the branch easily before any commit is added to it.

Now, in order to do so, we need two things in our script: the commit message and the branch name. The commit message will be provided to the hook as a parameter, while in order to get the branch name we will need to execute a git branch command inside our node.js script.

Gathering parameters

Parameters passed to a node shell script can be accessed from the special process.argv array. The process module is a special node object hat is accesible from every node file or module, just like the module object.

In every node script, the first two parameters will be the javaScript interpreter -node- and the script filename. So, parameters provided to our script will be found in process.argv[2] and more. There are dedicated node packages to handle with command line arguments, like minimist, but is not like we want to add a node_modules to our hooks folder. We are just grabbing one single argument, so just:

process.argv[2]

will do the trick.

Now, I said that the commit message will be passed as an argument.That’s, well, not exactly true, because if we log process.argv[2], we will see this:

.git/COMMIT_EDITMSG

Which is actually a filename, which in turn contains the commit message -this time is true-. So, in order to get the commit message, we need to read the file, like in the following snippet:

(notice how we are requiring the fs module. Native node modules like fs,path or util are always available in node shell scripts).

You may read before that you must never use synchronous api’s in node, like readFileSync(). Well, that’s true for code being ran in a web server, but in a shell script is absolutely safe to use synchronous api’s to grab things from the filesystem.

That snippet ahead just reads the filename at process.argv[2], that contains the commit message. Passing an encoding as the second parameter is mandatory to get a string instead of a buffer. The trim() call just prevents extra spaces or carriage returns characters to get into our commit message.

Getting the current branch

Sadly, the git branch is not passed to the hook as a parameter, so we will need to grab it from the OS. We can execute any command from a node script using the child_process module. child_process exports an object which provides two methods to handle with OS external processes: exec and spawn. Both are asynchronous, and spawn on top of that will return a stream, while exec will return a buffer or a string. When dealing with commands that output large amount of data spawn is the way to go, but to capture the output of a git branch call, exec will suffice, and it’s easier to use.

Just like in the official exec documentation, we will use the util module to promisify the exec call. A way to get the git branch output in a node script will be like the following:

The exec callback, apart from the error first parameter, has two parameters: stdout and stderr. The command output will, if all works as expected, be passed in the stdout parameter, which means that, when we promisify and await for the exec call, we need to check the stdout field.

The previous script will print a text like the following:

* feature/HP-100-new-feature

master

feature/HP-101-another-branch

Just a list of lines with the branch names, and the current branch marked with an asterisk. Now, to get the current branch, we need to select the one with an asterisks. Let’s say we captured the output in a branches variable, the next statement will return the currently selected branch:

const current = branches.split('\n').find(b => b.charAt(0) === '*')

.trim().substring(2);

This just splits the lines into an array of lines, one per each branch, then finds the one that starts with the asterisk. The substring() call at the end just gets rid of the asterisk in the name, while the extra trim() prevents any undesired space or special char to slip into our current branch name variable.

Enforcing the naming policy

Now we know how to get the commit message and the branch name, so we just need to check if both of them comply with our naming policy. There are a lot of ways to do this: you can either start searching for specific substrings at the start of the messages, or use regular expression. For this particular case I prefer the latter. The following regular expressions will express the naming policies for the branch names and the commit messages:

const BRANCH_CONTRACT = /^(feature|hotfix)\/AP-[0–9]{1,6}-/;

const CODE_CONTRACT = /AP-[0–9]{1,6}-/;

In order to check if a string match a regex, we can just use the regex test() method:

BRANCH_CONTRACT.test('bad-branch-name'); //false

CODE_CONTRACT.test('AP-501-added-favicon.ico'); //true

Creating the Hook

We now know all we needed to create a node shell script as our commit-msg git hook. The following snippet is a valid git hook that will ensure that our commit messages and branch names follow our naming policy:

Caveats

If you use Source Tree -or, I guess, any git GUI, at least on MacOs, which is my current environment-, and add this hook to your project, you will see how Source Tree starts complaining about not finding node.

Following this SO thread, there is a way to fix this, just by calling source tree from the command line (like open /Applications/SourceTree.app/Contents/MacOS/SourceTree). It’s pretty awkward, but it works.

--

--

Sergeon
Sergeon

Written by Sergeon

A Spanish software artisan from Madrid, passionate about programming, sequential puzzles, sci-fi and blockchain technologies.

Responses (3)