hooksman 1.0.2 hooksman: ^1.0.2 copied to clipboard
Create git hooks and tasks using Dart scripts and Shell commands
Hooksman (Hooks Manager) #
Overview #
The hooksman
package allows you to manage and execute Git hooks using Dart. You can define your hooks as Dart files and register them with Git to run automatically when the corresponding events occur (e.g., pre-commit, post-commit, etc.). Inspired by lint-staged and husky, hooksman
provides a flexible and powerful way to automate tasks during your workflow and share them across your team.
With hooksman
you can run shell commands, Dart code, or a combination of both in your hooks to enforce coding standards, run tests, or perform other tasks.
Tasks are used to safeguard your codebase, if a task fails, hooksman
exits with a non-zero status code, preventing the hook from completing (like a pre-commit hook).
Installation #
Add hooksman
to your pubspec.yaml
:
dart pub add hooksman --dev
Then, run dart pub get
to install the package.
Register Hooks #
To register your hooks with Git, run the following command:
dart run hooksman
This command will compile your hooks and copy the executables to the hooks directory.
Warning
The hooksman
package will overwrite all existing hooks in the .git/hooks
directory with the new hooks. Make sure to back up any existing hooks before running the hooksman
command.
Hooks #
Create Hooks Directory #
Create a hooks
directory in the root of your project to store your hooks.
.
├── hooks
├── lib
│ └── ...
└── pubspec.yaml
Create Hook #
Create your hooks as Dart files in the hooks
directory. Each file should contain a main
function that returns a Hook
object, imported from the hooksman
package.
import 'package:hooksman/hooksman.dart';
Hook main() {
return Hook(
... // Tasks
);
}
Note
hooksman
scans the hooks
directory for Dart files to use as hooks. You can organize your code by placing additional Dart files in subdirectories within the hooks
directory. These files can be imported into your hook files and will not be picked up by hooksman
as hooks.
.
└── hooks
├── tasks
│ ├── some_dart_task.dart # ignored
│ └── ...
└── pre_commit.dart # picked up
Hook Names #
The name of the hook is derived from the file name. For example, a file named pre_commit.dart
will be registered as the pre-commit
hook. Be sure to follow the naming convention for Git hooks.
Tip
Look at the git hooks documentation for more information on the available hooks: Git Hooks Documentation.
Tasks #
Tasks are modular units of work that you define to be executed during specific Git hook events. They allow you to automate checks, validations, or any custom scripts to ensure code quality and consistency across your repository. Tasks are powerful because they can be customized to suit your project's needs while targeting specific file paths or patterns.
All top level tasks are executed in parallel, while tasks within a group are executed sequentially. This allows you to run multiple tasks concurrently and group related tasks together.
File Patterns #
You can specify file patterns to include or exclude from a task using any Pattern
object (Glob
, RegExp
, String
, etc.). Each task can have multiple include and exclude patterns.
Tip
hooksman
exposes the Glob
class from the Glob package to match file paths using glob patterns.
hooksman
also has an AllFiles
class to match all file paths.
Note
exclude
filters any matching files before include
is applied.
After the filters are applied, the remaining files are passed to the task's commands
or run
function.
Task Naming #
Each task is assigned a name that is displayed when the task is executed. This is useful for identifying the task in the output. By default, the name of the task is the pattern(s) used to include files. If you would like to provide a custom name, you can do so by setting the name
property of the task.
ShellTask(
name: 'Analyze',
include: [Glob('**.dart')],
exclude: [Glob('**.g.dart')],
commands: (filePaths) => [
'dart analyze --fatal-infos ${filePaths.join(' ')}',
],
),
Shell Task #
A ShellTask
allows you to run shell commands.
ShellTask(
name: 'Analyze',
include: [Glob('**.dart')],
exclude: [Glob('**.g.dart')],
commands: (filePaths) => [
'dart analyze --fatal-infos ${filePaths.join(' ')}',
],
),
Dart Task #
A DartTask
allows you to run Dart code.
DartTask(
include: [Glob('**.dart')],
run: (filePaths) async {
print('Running custom task');
return 0;
},
),
Sequential Tasks #
You can group tasks together using the SequentialTasks
class, which runs the tasks sequentially, one after the other.
SequentialTasks(
tasks: [
ShellTask(
include: [Glob('**.dart')],
commands: (filePaths) => [
'dart format ${filePaths.join(' ')}',
],
),
ShellTask(
include: [Glob('**.dart')],
commands: (filePaths) => [
'sip test --concurrent --bail',
],
),
],
),
Tip
Check out sip_cli
for a Dart-based CLI tool to manage mono-repos, maintain project scripts, and run dart|flutter pub get
across multiple packages.
Parallel Tasks #
You can group tasks together using the ParallelTasks
class, which runs the tasks in parallel.
ParallelTasks(
tasks: [
ShellTask(
include: [Glob('**.dart')],
commands: (filePaths) => [
'dart format ${filePaths.join(' ')}',
],
),
ShellTask(
include: [Glob('**.dart')],
commands: (filePaths) => [
'sip test --concurrent --bail',
],
),
],
),
Tip
Check out sip_cli
for a Dart-based CLI tool to manage mono-repos, maintain project scripts, and run dart|flutter pub get
across multiple packages.
Predefined Tasks #
ReRegisterHooks #
It can be easy to forget to re-register the hooks with Git after making changes. Re-registering the hooks is necessary to ensure that the changes are applied, since your dart files are compiled into executables then copied to the .git/hooks
directory.
To automate this process, you can use the ReRegisterHooks
task. This task will re-register your hooks with Git whenever any hook files are created, modified, or deleted.
Hook main() {
return Hook(
tasks: [
ReRegisterHooks(),
],
);
}
Tip
If your hooks
directory is not found in the root of your project, you can specify the path to the hooks
directory to the ReRegisterHooks
task.
ReRegisterHooks(pathToHooksDir: 'path/to/hooks'),
Hook Execution #
The hooks will be executed automatically by Git when the corresponding events occur (e.g., pre-commit, post-commit, etc.).
Amending to the Commit #
hooksman
, similar to lint-staged
, will add any created/deleted/modified files to the commit after running the tasks. Ensuring that the changes are included in the commit.
An example of this behavior is when you have a ShellTask
that formats the code using dart format
. If the code is not formatted correctly, hooksman
will format the code and add the changes to the commit.
Partially Staged Files #
A partially staged file is when you have some_file.dart
staged in the index, along with some changes to the same some_file.dart
file in the working directory.
When the tasks are executed, hooksman
will stash any non-staged changes to partially staged files, run the tasks, then pop the stash with the changes. The is beneficial because it ensures that the tasks are run on the version of the file that is staged in the index, rather than the working directory.
Error Handling #
If an error occurs during the execution of a task, hooksman
will stop the execution of the remaining tasks and exit with a non-zero status code. This will prevent the commit from being made, allowing you to fix the issue before committing again.
Signal Interruption #
If the user interrupts the hook execution (e.g., by pressing Ctrl+C
), hooksman
will stop the execution of the remaining tasks and exit with a non-zero status code. Extra precautions are taken to ensure that the repository is left in a clean state.
Configuration #
Hooks are initially setup to run for the pre-commit
hook, but you can configure the hooks to run for other events by specifying the diff
and diffFilters
parameters in the Hook
object.
Diff Filters #
The diffFilters
parameter allows you to specify the statuses of files to include or exclude, such as added
, modified
, or deleted
.
Hook main() {
return Hook(
diffFilters: 'AM', // Include added and modified files
tasks: [
...
],
);
}
Diff #
The diff
parameter allows you to specify how files are compared with the working directory, index, or commit.
The example below demonstrates how to compare files with the remote branch (e.g., origin/main
). This could be useful for a pre-push
hook.
Hook main() {
return Hook(
diffArgs: ['@{u}', 'HEAD'], // Compare files with the remote branch
tasks: [
...
],
);
}
Verbose Output #
You can enable verbose output by using the VerboseHook
class. This will slow down the execution of the tasks and output detailed information about the tasks being executed. This can be useful to understand the order of execution and the files being processed. This is not intended to be used in non-developing environments.
Example #
// hooks/pre_commit.dart
import 'package:hooksman/hooksman.dart';
Hook main() {
return Hook(
tasks: [
ReRegisterHooks(),
ShellTask(
name: 'Lint & Format',
include: [Glob('**.dart')],
exclude: [
Glob('**.g.dart'),
],
commands: (filePaths) => [
'dart analyze --fatal-infos ${filePaths.join(' ')}',
'dart format ${filePaths.join(' ')}',
],
),
ShellTask(
name: 'Build Runner',
include: [Glob('lib/models/**.dart')],
exclude: [Glob('**.g.dart')],
commands: (filePaths) => [
'sip run build_runner build',
],
),
ShellTask(
name: 'Tests',
include: [Glob('**.dart')],
exclude: [Glob('hooks/**')],
commands: (filePaths) => [
'sip test --concurrent --bail',
],
),
],
);
}
License #
This project is licensed under the MIT License.