Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 32 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## mbx build cache

`mise install` installs mbx 1.6. `mise run` activates the project's transparent
`mise install` installs the configured mbx version. `mise run` activates the project's transparent
Cargo wrapper, so compilation-heavy mise tasks and hk checks use ordinary
`cargo` commands. Standalone Cargo commands require an activated mise shell. If
the wrapper fails or creates a development papercut, rerun the exact equivalent
Expand All @@ -24,6 +24,7 @@ SHOULD use the same format:
**Format:** `<type>(<scope>): <description>`

**Types:**

- `feat:` - New features
- `fix:` - Bug fixes that affect the CLI behavior (not CI, docs, or infrastructure)
- `refactor:` - Code refactoring
Expand All @@ -37,15 +38,18 @@ SHOULD use the same format:
- `revert:` - Reverting a previous change

**Scopes:**

- For command-specific changes, use the command name: `check`, `fix`, `run`, `init`, `install`, `validate`, etc.
- For subsystem changes: `hook`, `step`, `config`, `lock`, `pkl`, `builtins`, `stash`, `deps`

**Description Style:**

- Start the description with a lowercase character
- Use imperative mood ("add feature" not "added feature")
- Keep it concise but descriptive

**Examples:**

- `fix(step): resolve race condition in file locking`
- `feat(check): add --slow flag for expensive linters`
- `feat(builtins): add biome linter`
Expand All @@ -66,11 +70,13 @@ imperative mood remains a review rule.
## Development Commands

**Build the project:**

```bash
mise run build
```

**Run tests:**

```bash
# Run all tests (Rust unit tests + bats integration tests)
mise run test
Expand All @@ -89,6 +95,7 @@ mise run test:bats test/check.bats
```

**Lint and format code:**

```bash
# Run all linters and checks
hk check --all
Expand All @@ -103,54 +110,68 @@ hk fix --all --slow

hk is a git hook manager and project linting tool written in Rust with emphasis on performance and concurrent execution. The architecture leverages file locks to maximize concurrency while preventing race conditions.

### Workspace Structure
### Crate Structure

The root Cargo package builds the **hk** CLI and the **generate-docs** utility.
It depends on separately published crates for shared functionality:

The project is a Cargo workspace with these crates:
- **hk** (root): Main CLI application
- **xx**: HTTP client and utility library
- **xx**: HTTP client and utilities
- **clx**: CLI/terminal UI utilities (progress indicators, styling)
- **ensembler**: Script/command execution engine

### Core Components

**Configuration System (src/config.rs):**

- Main config file: `hk.pkl` in project root
- Uses Pkl (github.com/apple/pkl) as the configuration language
- Config amends a base schema from `pkl/Config.pkl`

**Hook System (src/hook.rs):**

- Manages git hooks (pre-commit, pre-push, commit-msg, prepare-commit-msg)
- Supports custom hooks like "check" and "fix" for manual runs
- Implements stashing strategies for git hooks
- Handles concurrent step execution with proper locking

**Step Execution (src/step/):**

- Steps are individual linting/formatting tasks
- Each step can have: check, fix, shell commands
- Steps support glob patterns for file filtering
- Steps can depend on other steps
- Steps use read/write file locks to prevent conflicts

**File Locking (src/file_rw_locks.rs):**

- Implements a sophisticated file locking system
- Allows multiple readers or single writer per file
- Prevents race conditions during concurrent execution
- Critical for maximizing parallelism

**Built-in Linters (pkl/builtins/):**

- Extensive library of pre-configured linters and formatters
- Each builtin is a Pkl file defining step configuration
- Used via `Builtins.linter_name` in hk.pkl

**CLI Interface (src/cli/):**

- Subcommands: init, install, uninstall, check, fix, run, validate, config
- Uses clap for argument parsing
- Uses usage-rs for argument parsing
- Supports running specific hooks or steps

### Documentation

- Preview or build the website with `mise run docs` or `mise run docs:build`.
- Edit generated reference content at its source: `pkl/Config.pkl`, `settings.toml`, builtin definitions, and Rust CLI help comments.
- `scripts/enrich-cli-docs.py` adds maintained examples after CLI reference generation.
- Example pages include `docs/public/*.pkl` directly. Validate them with `scripts/generate-examples.sh` in the mise environment.

### Key Design Patterns

1. **Concurrent Execution:** Steps run in parallel when possible, using tokio for async runtime
2. **File-based Coordination:** Uses file locks instead of in-memory coordination for cross-process safety
2. **File-based Coordination:** Uses in-memory read/write locks keyed by file path to coordinate steps within a hook run
3. **Pluggable Configuration:** Pkl-based config allows easy extension and customization
4. **Progressive Enhancement:** Works with or without git, libgit2, mise, etc.

Expand All @@ -163,26 +184,30 @@ The project is a Cargo workspace with these crates:
### Testing

Bats integration tests are in `test/*.bats`. Each test file uses a common setup pattern:

```bash
setup() {
load 'test_helper/common_setup'
_common_setup
}
```

Tests run in isolated temp directories with a clean git repo. The `$PKL_PATH` variable points to the pkl config directory for amending `Config.pkl`.

#### Testing Builtins

Builtins should have pkl-level tests defined via the `tests` field on the Step (see `pkl/Config.pkl` `StepTest`). These tests are run by `hk test` and exercised in CI via `test/builtins_tests.bats`, which loads all builtins and runs their tests.

**Tool stubs** in `test/builtin_tool_stubs/` use `mise tool-stub` to auto-install the correct tool version on demand. Each stub is a small script:

```bash
#!/usr/bin/env -S mise tool-stub
version = "2"
tool = "aqua:golangci/golangci-lint"
```

To add a new builtin with tests:

1. Define the builtin in `pkl/builtins/<name>.pkl` with a `tests` block
2. Add a tool stub in `test/builtin_tool_stubs/<tool-name>` if the tool isn't already available
3. Use the `TestMaker` helper from `pkl/builtins/test/helpers.pkl` for standard check/fix test patterns
Expand Down
16 changes: 14 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
# Contributing
# Contributing to hk

See the [contributing guide](https://hk.jdx.dev/contributing).
Read the [contributing guide](docs/contributing.md) for review expectations, development setup, and how to add a builtin. It is also available on the [documentation website](https://hk.jdx.dev/contributing).

To get a checkout ready:

```sh
mise install
mise run build
mise run test
```

For a documentation change, use `mise run docs` to preview the site and `mise run docs:build` to validate it. Generated reference content has its own source files; see [editing documentation](docs/contributing.md#edit-documentation).

PR titles must use Conventional Commits; use the same format for intermediate commits where practical, for example `fix(step): handle missing files` or `docs: clarify hook installation`.

## mbx build cache

Expand Down
111 changes: 44 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,63 +1,39 @@
# hk

A fast, language-agnostic git hook manager and project linter.
**Git hooks and project checks, in parallel.**

hk runs linters concurrently while coordinating access to files with read/write locks. This lets
formatters and other tools safely work on overlapping files without racing or silently overwriting
one another's changes.
hk runs linters and formatters with read/write file locks, so independent work runs concurrently and tools that modify the same files take turns. Use the same steps in Git hooks, from your terminal, and in CI.

- Runs independent checks and fixes in parallel
- Safely handles partially staged files by stashing and restoring unstaged changes
- Includes [built-in configurations](https://hk.jdx.dev/builtins) for common linters and formatters
- Uses typed [Pkl](https://pkl-lang.org/) configuration
- Integrates with [mise](https://mise.jdx.dev/) for tool and task management
- Provides fast native checks for common issues such as trailing whitespace and merge conflicts
[Get started](https://hk.jdx.dev/getting_started) · [Documentation](https://hk.jdx.dev/) · [Built-in linters](https://hk.jdx.dev/builtins) · [CLI reference](https://hk.jdx.dev/cli/)

## Quick start

From the project you want to configure, install hk with mise:
Install with [mise](https://mise.jdx.dev/), then run these commands inside your repository:

```sh
cd my-project
mise use hk
hk --version
```

With Git 2.54 or newer, install hk's hooks once for every repository on your machine:

```sh
hk install --global
```

Then enable hk in a project:

```sh
hk init
hk install
hk check --all
```

`hk init` detects relevant linters and creates an `hk.pkl` configuration. Review the detected
linters, or use `hk init --interactive` to select them yourself. You can then commit as usual; hk
runs the configured `pre-commit` hook automatically. Repositories without an `hk.pkl` are left
untouched by the global hooks.
`hk init` detects project tools and generates `hk.pkl`. Review the selected linters and make sure their executables are available on `PATH`; hk configures how to run them, but does not install them. Use `hk init --interactive` to choose tools yourself.

On older Git versions, run `hk install` in each project instead. See the
[getting started guide](https://hk.jdx.dev/getting_started) for Homebrew, Cargo, and Aqua installation
options and detailed hook setup.
On Git 2.54+, you can run `hk install --global` once to enable hk across repositories. Installed hooks exit silently in projects without an hk configuration. See [installation options](https://hk.jdx.dev/getting_started#install-hooks) for older Git versions and mise environments.

## Example configuration
You can also install hk with `brew install hk` or `cargo install hk --locked`. The default Pkl evaluator is built into hk; a separate Pkl installation is optional.

The generated `hk.pkl` uses hk's built-in linter definitions, which you can extend when a project
needs different behavior:
## A configuration you can share

This example uses hk’s built-in whitespace utilities, so it needs no additional linter:

```pkl
amends "package://github.com/jdx/hk/releases/download/v1.58.1/hk@1.58.1#/Config.pkl"
import "package://github.com/jdx/hk/releases/download/v1.58.1/hk@1.58.1#/Builtins.pkl"

local linters = new Mapping<String, Step> {
["eslint"] = Builtins.eslint
["prettier"] = (Builtins.prettier) {
glob = List("*.js", "*.ts", "*.json", "*.md")
}
["trailing-whitespace"] = Builtins.trailing_whitespace
["newlines"] = Builtins.newlines
}

hooks {
Expand All @@ -66,23 +42,40 @@ hooks {
stash = "git"
steps = linters
}
["check"] {
steps = linters
}
["check"] { steps = linters }
["fix"] {
fix = true
steps = linters
}
}
```

Run the same checks directly at any time:
Add tools such as `Builtins.prettier`, `Builtins.eslint`, or `Builtins.ruff`, or [define your own steps](https://hk.jdx.dev/reference/examples/custom-linters).

```sh
hk check # check modified files
hk fix # fix modified files
hk check --all # check the entire repository, useful in CI
```
## Everyday commands

| Command | Use it to |
| ------------------------- | ----------------------------------------------------- |
| `hk check` | Check modified files |
| `hk fix` | Apply available fixes to modified files |
| `hk check --all` | Check the repository, including in CI |
| `hk check --plan` | Preview selected files and steps without running them |
| `hk check --why prettier` | Explain why a step will run or be skipped |

By convention, checks do not modify files. Fixes may modify and stage files; review `git diff` and `git diff --cached`. Use `hk fix --no-stage` to leave fixes unstaged. The generated pre-commit hook stashes unstaged work before fixing staged files, then restores it afterward. [Learn about hooks and partial commits](https://hk.jdx.dev/hooks).

## Why hk?

- **Coordinate concurrent tools.** File locks protect overlapping steps; diff and file-list checks reduce the work that needs exclusive access.
- **Reuse linter configurations.** Builtins describe file patterns, check commands, fixes, and tool-specific optimizations.
- **Keep configuration maintainable.** Pkl provides types, imports, and reusable objects for sharing steps across hooks and projects.
- **Use your existing toolchain.** Run commands from `PATH`, or use [mise](https://hk.jdx.dev/mise_integration) to manage tools and environments.

Read [how hk works](https://hk.jdx.dev/why-hk), browse [project examples](https://hk.jdx.dev/reference/examples/), or see the [benchmark methodology and results](https://hk.jdx.dev/benchmarks).

## Demo

![hk running project checks](docs/public/hk-demo.gif)

## Agent skills

Expand All @@ -98,15 +91,9 @@ to those directories, so compatible installers can use the bundled instructions
repository download. Making them available to an agent is
opt-in; see [mise's skills documentation](https://mise.jdx.dev/dev-tools/packslip-resources.html).

## Documentation
## Contributing

- [Getting started](https://hk.jdx.dev/getting_started)
- [Configuration reference](https://hk.jdx.dev/configuration)
- [Configuration examples](https://hk.jdx.dev/reference/examples/)
- [Built-in linters](https://hk.jdx.dev/builtins)
- [CLI reference](https://hk.jdx.dev/cli/)
- [Why hk?](https://hk.jdx.dev/why-hk)
- [Contributing](CONTRIBUTING.md)
See the [contributing guide](CONTRIBUTING.md) for development setup, tests, and review expectations. hk is released under the [MIT license](LICENSE).

## Sponsors

Expand All @@ -129,16 +116,6 @@ opt-in; see [mise's skills documentation](https://mise.jdx.dev/dev-tools/packsli
<a href="https://jdx.dev/sponsors.html">View all sponsors</a>
</p>

## Demo

![hk demo](docs/public/hk-demo.gif)

## CI

<p>
<a href="https://namespace.so">
<img src="docs/public/namespace-logo.svg" alt="Namespace" width="64">
</a>
</p>

Thanks to [Namespace](https://namespace.so) for providing CI for hk.

<a href="https://namespace.so"><img src="docs/public/namespace-logo.svg" alt="Namespace" width="64"></a>
4 changes: 3 additions & 1 deletion bin/generate_docs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ fn generate_settings_doc() -> Result<(), Box<dyn std::error::Error>> {
// Include per-setting docs as collapsible sections
for key in &keys {
let opt = registry.options.get(key).unwrap();
md.push_str(&format!("### `{}`\n\n", key.replace('_', "-")));
md.push_str(&format!("### `{key}`\n\n"));
// Metadata: unordered list with type, default (if any), and sources
md.push_str(&format!("- Type: `{}`\n", opt.typ));
if let Some(default) = &opt.default {
Expand Down Expand Up @@ -308,6 +308,8 @@ fn generate_builtins_doc() -> Result<(), Box<dyn std::error::Error>> {
let display_name = info.name.replace('_', "-");
md.push_str(&format!("### `{}`\n\n", display_name));

md.push_str(&format!("**Pkl:** `Builtins.{}`\n\n", info.name));

if !info.description.is_empty() {
md.push_str(&format!("{}\n\n", info.description));
}
Expand Down
Loading
Loading