# Leveraging Lefthook to enforce commit guidelines at exile.watch

<figure><img src="https://1213438767-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F4DXEoZIzUdpt18CW2qTD%2Fuploads%2FzXjtfpnPjXaN7gAZDm9L%2Fexilewatch.webp?alt=media&#x26;token=74fc8000-b114-47be-bc13-cda9b75cd22f" alt="" width="256"><figcaption><p>exile.watch logo</p></figcaption></figure>

When it comes to collaboration with others (whether from the same organization, team, or in open source), having clear rules is essential. No one really enjoys chaos, and if you do, then godspeed.

Focusing on architecture from the start may bring some pain and frustration, but as time flies, a well-set architecture will save you far more headaches and money.

One of the key rules and architectural aspects are [git hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks).

## What are git hooks?

[Git](https://git-scm.com/) has a way to fire off custom scripts when certain important actions occur.&#x20;

There are two groups of these hooks: client-side and server-side.&#x20;

Client-side hooks are triggered by operations such as [committing](https://en.wikipedia.org/wiki/Commit_\(version_control\)) and [merging](https://en.wikipedia.org/wiki/Merge_\(version_control\)), while server-side hooks run on network operations like receiving pushed commits.

In this post, we will solely focus on client-side hooks, setting the stage for an efficient management system.

## Git hooks managers

Understanding git hooks sets the stage, but managing them efficiently is where git hooks managers come into play.&#x20;

The first thing you'll likely find about git hooks is the official [Git Hooks documentation](https://git-scm.com/docs/githooks).&#x20;

However, there are already git hooks managers available that are fast, well-maintained, and battle-tested, so I didn't see the need to create a custom git hook manager.&#x20;

To name a few: [Husky](https://github.com/typicode/husky), [pre-commit](https://github.com/pre-commit/pre-commit), [overcommit](https://github.com/sds/overcommit), [and more](https://github.com/aitemr/awesome-git-hooks)!

## What about Lefthook?

While there are several git hooks managers out there, my googling led me to a particular one that stood out—[Lefthook](https://github.com/evilmartians/lefthook).&#x20;

I've always worked with Husky, but ever since the [v5 fiasco](https://github.com/typicode/husky/issues/857), it became clear that, at the time, this was an unstable project that breached the trust.&#x20;

When I gave [Husky v9](https://github.com/typicode/husky/releases/tag/v9.0.1) a shot a few weeks ago, I'll be honest: I had no clue how to create a shareable configuration and was somewhat wary.

Add [lint-staged](https://github.com/lint-staged/lint-staged), commonly used with Husky, to the mix, and you get a sense that things could have been simpler in 2024.

## Before we dive into Lefthook, let's quickly mention commit guidelines

If you visit [commit message guidelines](https://docs.exile.watch/development/commit-message-guidelines) at <https://docs.exile.watch/>  you will notice following banner at the very top:

{% hint style="danger" %}
Your contributions will be rejected if you won't follow this guideline.
{% endhint %}

With a clear framework for commit messages established, integrating Lefthook to enforce these standards was our next step, particularly within our Lerna environment.&#x20;

### A word about commitizen

[Commitizen](https://commitizen.github.io/cz-cli/) is a [CLI](https://en.wikipedia.org/wiki/Command-line_interface) tool designed to help developers create more consistent and structured commit messages following conventional commit guidelines.

### A word about commitlint

[commitlint](https://commitlint.js.org/) checks if commit messages meet specified guidelines, playing a crucial role in our commit process.

## Cooking up Lefthook in Lerna environment

If this is your first encounter with [Lerna](https://lerna.js.org/) and how it's applied at exile.watch, I suggest giving [lerna-the-hidden-powerhouse-of-exile.watch](https://engineering.exile.watch/march-2024/lerna-the-hidden-powerhouse-of-exile.watch "mention") a read.&#x20;

If you're short on time, here's the gist: exile.watch relies heavily on dependencies structured as boilerplates for consumer applications to use.&#x20;

And guess what? Lefthook is one of these crucial "boilerplates."

***

Lefthook finds its place within the [@exile-watch/splinters](https://github.com/exile-watch/splinters) build tools [monorepo](https://monorepo.tools/), specifically under the [@exile-watch/lefthook-config](https://github.com/exile-watch/splinters/tree/main/packages/lefthook-config) package.&#x20;

To give you an idea, here's how the `@exile-watch/splinters` repo is organized, focusing just on the Lefthook part:

```
// Simplified @exile-watch/splinters repo structure
.
├── .lefthook
│   └── commit-msg
│       └── commitlint.sh
├── packages
│   └── lefthook-config
│       ├── scripts
│       │   └── commitlint.sh
│       ├── README.md
│       ├── lefthook.yml
│       └── package.json
├── lefthook.yml
├── lerna.json 
├── commitlint.config.js
└── package.json  // @exile-watch/lefthook-config as a devDependency
```

Right off the bat, 4 key components stand out:

1. `.lefthook/commit-msg` with`commitlint.sh` bash script at the root of the repository
2. `lefthook-config` having `scripts` directory with `commitlint.sh`
3. `lefthook.yml` in both - the root of the repository *and* `lefthook-config`
4. `commitlint.config.js`

Let's delve into the roles each plays.

## The dual role of commitlint.sh

Diving into the specifics, let's tackle an essential caveat first:

[If you find yourself with more than one Lefthook file in the project, be warned—only one will be actively used, and it's anyone's guess which one that might be.](https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md#config-file)

This situation could mess up our setup.&#x20;

To avoid this problem, we make sure the root `.lefthook/commit-msg/commitlint.sh` script calls the script inside the package directly:

```bash
# .lefthook/commit-msg/commitlint.sh - our root repo .lefthook config

#!/bin/bash
source node_modules/@exile-watch/lefthook-config/scripts/commitlint.sh

run_commitlint
```

```bash
# lefthook-config/scripts/commitlint.sh - our lefthook-config package
#!/bin/bash
function run_commitlint {
  echo $(head -n1 $1) | npx commitlint --edit --color --help-url='https://docs.exile.watch/development/commit-message-guidelines'
}
```

This setup essentially ensures that our root `.lefthook`'s `commitlint.sh` script activates the script defined in the package itself, thereby enforcing our commit guidelines directly from the package.

By doing so, we maintain control and clarity, using the root script to trigger the package-defined script, thereby unifying our commit linting process under one predictable mechanism.

One more note: [The `/scripts` directory ](https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md#source_dir)is also [required](https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md#remotes) to make remotes work—more on that later.

## Why Lefthook and commitlint are essential

Our primary aim is to uphold commit standards, necessitated by `commitlint`, which avails the `commitlint` command.&#x20;

The question then becomes: how best to activate our `commitlint` package?&#x20;

Through the `commitlint.sh` bash script, of course.

This crucial detail explains why having the `@exile-watch/lefthook-config` as a [devDependency](https://stackoverflow.com/questions/18875674/whats-the-difference-between-dependencies-devdependencies-and-peerdependencie/22004559#22004559) is vital—it empowers us to execute Lefthook within the same repository where the package resides:

```bash
# Enabling the use of Lefthook's commitlint within the same repo
source node_modules/@exile-watch/lefthook-config/scripts/commitlint.sh
```

And by invoking `run_commitlint`, we kickstart the [linting process](https://en.wikipedia.org/wiki/Lint_\(software\)) as defined within our package's `commitlint.sh` script.&#x20;

***

The naming conventions here are flexible; I opted for simplicity and consistency across the board, hence the recurrent use of "commitlint."

### Deciphering the commitlint.sh script

Let's break down this script bit by bit:

```bash
echo $(head -n1 $1) | npx commitlint --edit --color --help-url='https://docs.exile.watch/development/commit-message-guidelines'
```

* **$1** and **head -n1 $1**: Fetches and echoes the first line of the commit message, preparing it for linting.
* **npx commitlint**: Executes commitlint, applying our project's rules to the commit message in question.
* **--edit, --color, --help-url**: [Options](https://commitlint.js.org/reference/cli.html) enhancing usability, like colored output and providing a URL for guideline assistance.

## Integrating Lefthook with commitlint.sh

But how do we ensure Lefthook autonomously triggers this script at the[ pre-commit stage](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#_committing_workflow_hooks)? That's where `lefthook.yml` within `lefthook-config` comes into play

<pre class="language-yaml"><code class="lang-yaml"><strong># {root}/packages/lefthook-config/lefthook.yml
</strong><strong>pre-commit:
</strong>  parallel: true
  commands:
    lint:
      glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,yml,yaml}"
      run: npx biome check --no-errors-on-unmatched --files-ignore-unknown=true {staged_files}

    "lint:apply":
      glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,yml,yaml}"
      run: npx biome check --apply --no-errors-on-unmatched --files-ignore-unknown=true {staged_files} &#x26;&#x26; git update-index --again

    "typecheck":
      glob: "*.{ts,tsx}"
      run: npx tsc

commit-msg:
  scripts:
    "commitlint.sh":
      runner: bash
</code></pre>

Notice this part:

```bash
commit-msg:
  scripts:
    "commitlint.sh":
      runner: bash
```

This configuration informs Lefthook about the `commitlint.sh` script, ensuring it's triggered appropriately.

## Eating the cooked lefthook-config package in other repositories

The brilliance and simplicity of extending this setup to other repositories lie in the `lefthook.yml` file at the root, pointing to our configured `lefthook.yml` within the `lefthook-config` package:

```yaml
# {root}/lefthook.yml
remotes:
  - git_url: https://github.com/exile-watch/splinters
    configs:
      - packages/lefthook-config/lefthook.yml
```

You're essentially instructing Lefthook to fetch and merge the specified configurations, making project-wide guideline enforcement a breeze.

Check how [nucleus](https://github.com/exile-watch/nucleus) and [crucible](https://github.com/exile-watch/crucible) repos are consuming our [lefthook-config](https://github.com/exile-watch/splinters/tree/main/packages/lefthook-config).

## Understanding `commitlint.config.js`

Last component in our highlighted setup is [`commitlint.config.js`](https://commitlint.js.org/concepts/shareable-config.html) file.&#x20;

<pre class="language-javascript"><code class="lang-javascript"><strong>// {root}/commitlint.config.js
</strong><strong>module.exports = {
</strong>  extends: ["@commitlint/config-conventional"],
};
</code></pre>

```jsonp
// package.json
{
  // ... 
  "config": {
    "commitizen": {
      "path": "cz-conventional-changelog"
    }
  }
}
```

This [config](https://commitlint.js.org/concepts/shareable-config.html) sets our commit message rules, ensuring every message meets our project standards. It's key for keeping our git history clean and meaningful.

## Lastly, synchronizing our git hooks with a postinstall step

Before we evaluate our setup's complexity, there is one last step that we have to follow up with.&#x20;

After installing dependencies, we run Lefthook script:

```
// package.json
{
  // ... 
  "scripts": {
    // ...
    "postinstall": "npx lefthook install"
  }
}
```

By [post installing ](https://docs.npmjs.com/cli/v10/using-npm/scripts#life-cycle-operation-order)Lefthook, we are synchronizing our git hooks with latest changes that are provided by our lefthook-config package.

```flow
> postinstall
> npx lefthook install

sync hooks: ✔️ (pre-commit, commit-msg)
```

## Is this approach... Overcooked?

However, there's a bit of uncertainty on my end about whether this setup is the way it's supposed to work.&#x20;

The idea of a consumer package pulling its configuration from the `lefthook-config` package, which then executes a script from the root of its own repository, works like a charm in practice.&#x20;

Yet, it leaves me wondering if we've taken the simplicity of Lefthook's intended use and stretched it a bit too far.

[I've sparked a conversation on the Lefthook repository to clarify this very point](https://github.com/evilmartians/lefthook/discussions/639#discussioncomment-8591400).&#x20;

Ultimately, our approach works seamlessly for now, but it raises the question:&#x20;

Is this how it was meant to be used?&#x20;

Only time will tell if adjustments or a nod of approval await us down the line.

***

Author: [Sebastian Krzyżanowski](https://github.com/sbsrnt)\
About *exile.watch*: <https://docs.exile.watch/>\
Github: <https://github.com/exile-watch>\
\
Visit <https://exile.watch/> to experience it first hand
