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
47 changes: 47 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

`forge` is a single-binary, policy-driven git hook runner written in Go — a Node-free alternative to Husky/lint-staged/lefthook. Config lives in `forge.toml`; `forge install` sets git's `core.hooksPath` to `.forge/hooks`, whose shims shell out to `forge run <hook>`.

## Commands

```sh
make build # -> dist/forge (CGO off, -s -w -trimpath)
make install # build + copy to ~/.local/bin/forge
make test # go test -race -coverprofile=coverage.out ./...
make coverage # go tool cover -func on the last coverage.out
go build -o forge ./cmd/forge

go test ./internal/forge/runner/ -run TestName # single test
```

Toolchain is pinned in `mise.toml` (go 1.23.8, node 24, pnpm 11). `node`/`pnpm` are only for `website/`, not the binary.

## Architecture

Entry: `cmd/forge/main.go` → `forge.Run(args)` in `internal/forge/app.go`, a flat switch that dispatches every subcommand (`init install uninstall doctor completion cache list ci validate run migrate update`). New subcommand = new case here.

Packages under `internal/forge/`:

- **config** — TOML load/parse/merge and starter presets. `Config` → `Hooks[name]` → `Tools[name]`. Global config (`~/.config/forge/`) is merged under the repo config. `migrate.go` converts legacy Husky `.git-hooks.config.json`; `remote_preset.go` fetches `https://` presets for `init --preset URL`.
- **runner** — the core. `RunHookWithOptions` loads config, resolves workspace member, filters staged files, then executes tools sequentially (`runner.go`) or concurrently (`runner_parallel.go`). Also holds commit-msg policy enforcement (conventional-commit + ticket footer) and the run cache (`cache.go`, keyed on file+tool hashes; `--no-cache`/`cache clear` bypass it).
- **backend** — execution abstraction behind the `Backend` interface: `HostBackend`, `DdevBackend`, `DockerBackend`. `ResolveBackend` picks per-tool `backend` → `[execution].default_backend` → auto-detect (ddev if `.ddev/config.yaml` + container running via `docker inspect`). `ResolveCommandForBackend` rewrites the command for the container path.
- **git** — repo-root detection and staged-file listing (`git diff --cached --name-only --diff-filter=ACMR`).
- **install** — writes hook shims + embedded JSON schema to `.forge/`, sets `core.hooksPath`, adds entries to `.git/info/exclude`. `supportedHooks` in `install.go` is the authoritative hook list (pre-commit, commit-msg, pre-push, prepare-commit-msg, post-commit, post-merge, post-rewrite).
- **ui** — colored terminal output (`ui.UI` writer, `ui.Green/Dim/...`); `ci.go` is the plain-output variant.
- **update** — self-update from GitHub releases (`forge update`, `--check`, `--version`, `--rollback`).

## Non-obvious things

- **Tool order matters and Go maps don't preserve it.** `config.parseHookToolOrder` regex-scans the raw TOML for `[hooks.X.tools.Y]` sections so tools run in declaration order. If you touch config loading, keep `OrderedToolNames()` fed by that, not by map iteration.
- **The binary configures its own hooks via `forge.toml`** at the repo root (gofmt + go vet). Editing Go here triggers those on commit.
- **`forge.schema.json`** is embedded via `//go:embed` (`schema_embed.go`) and written out on install for editor autocompletion. Regenerate/update the file in `internal/forge/schema/` if config fields change.
- **`forge ci`** ≡ `run pre-commit --all-files --check --no-cache` — the CI-friendly, non-mutating path (`--check` uses `check_args`, suppresses restage, treats any output as failure).
- Skips are env-driven: `SKIP_<TOOL>=1`, `SKIP_GROUP_<GROUP>=1`, `SKIP_PRECOMMIT/COMMITMSG/PREPUSH=1`, `HOOKS_ONLY=group,...`, `FORGE_CONFIG=path`.

## Release

Automated via release-please (`release-please-config.json`) + goreleaser (`.goreleaser.yaml`). Commits must be conventional-commit format (the repo enforces its own commit-msg policy). Version/Commit/Date are injected into `app.go` vars at build time via ldflags.
14 changes: 9 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
# forge

A policy-driven git hook runner — fast, portable, no Node.js required.
**The git hook runner for containerized dev.** Run your linters and formatters *inside* DDEV or Docker — automatically, no wrapper scripts. A single Go binary, no Node.js required.

## Why forge?

Most hook runners (Husky, lint-staged) require Node.js and `package.json`. forge is a single Go binary that works in any project — PHP, Go, Python, or mixed monorepos.
If your tools live inside a container — `phpstan`, `ecs`, `php-cs-fixer` in a DDEV or Docker environment — every other hook runner makes you write wrapper scripts to shell into the container. forge routes tools to the container for you: set `backend = "ddev"` (or a container name), and forge auto-detects the running environment and executes the tool where it actually lives.

It's also a solid general-purpose runner: one binary, no `package.json`, works in PHP, Go, Python, or mixed monorepos.

| Feature | forge | Husky | lint-staged | lefthook |
|---------|---------|-------|-------------|----------|
| Single binary | ✅ | ❌ (needs Node) | ❌ (needs Node) | ✅ |
| **Runs hooks in DDEV / Docker** | ✅ | ❌ | ❌ | ❌ |
| **Commit-msg policy built in** | ✅ | ❌ | ❌ | ❌ |
| Single binary (no Node) | ✅ | ❌ (needs Node) | ❌ (needs Node) | ✅ |
| TOML config | ✅ | ❌ | ❌ | ✅ (YAML) |
| DDEV backend | ✅ | ❌ | ❌ | ❌ |
| Monorepo workspace mode | ✅ | ❌ | ✅ | ✅ |
| Commit-msg policy | ✅ | ❌ | ❌ | ❌ |
| Staged-file filtering | ✅ | ❌ | ✅ | ✅ |
| Migration from Husky | ✅ | — | — | ❌ |

The top two rows are what no other runner does — that's the reason forge exists. If you don't need container-aware hooks, lefthook is a fine choice too; forge earns its place when your toolchain lives in a container or you want commit-message policy without wiring up commitlint.

---

## Installation
Expand Down
90 changes: 90 additions & 0 deletions docs/brand-assets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# forge — brand & graphic asset prompts

Prompts for generating forge's visual assets (Google Stitch, or any image/design
generator). Keep the shared **Brand direction** identical across every asset so
the logo, favicon, and social card read as one system.

## Brand direction (paste at the top of every prompt)

> **Brand:** forge — a git hook runner that runs linters/formatters inside DDEV
> and Docker containers. Developer tool. The name evokes a blacksmith's forge:
> anvil, hammer, molten metal, sparks, heat.
>
> **Aesthetic:** modern developer-tool branding, in the spirit of Vite, Bun,
> Turborepo — bold, minimal, geometric, confident. Flat vector, not skeuomorphic.
> No gradients-as-crutch, no drop shadows, no 3D bevels, no stock-clipart look.
>
> **Palette:**
> - Molten orange/amber `#F97316` (primary, the "heat")
> - Deep amber `#EA580C` (accent)
> - Slate near-black `#0F172A` (dark surfaces / background)
> - Off-white `#F8FAFC` (light surfaces / text on dark)
>
> **Core mark:** a stylized **anvil** (instantly reads "forge", simple enough to
> shrink to a favicon), optionally with 2–3 small spark dots rising from it.
> Avoid words inside the mark.

---

## 1. Logo mark (square, primary)

Use for: site logo, README header, base for the favicon.

> [Brand direction above]
>
> Design a **square logo mark** on a transparent background. A single bold
> geometric **anvil** silhouette in molten orange `#F97316`, with 2–3 small spark
> dots in deeper amber `#EA580C` rising off the top-left horn. Thick, even
> strokes; strong negative space; must stay legible at 32×32 px. No text, no
> letters, no background, no shadow. Flat vector style. Provide on transparent
> and also on a `#0F172A` dark square.
>
> Output: 512×512 PNG (transparent) + SVG if available.

## 2. Favicon

Use for: `website/public/favicon.ico` and browser tab.

> [Brand direction above]
>
> A **radically simplified** version of the forge anvil mark for a 16×16 /
> 32×32 favicon. Just the anvil silhouette, one solid color: molten orange
> `#F97316` anvil on a `#0F172A` rounded-square background. No sparks (too small
> to read), no text, no detail that disappears when tiny. High contrast, chunky.
>
> Output: 512×512 PNG (I'll downscale to .ico at 16/32/48).

## 3. Open Graph social card

Use for: `og:image` / `twitter:image` — shown in Slack, X, Discord, LinkedIn link
previews. **Must be exactly 1200×630 px PNG** (SVG is unreliable for crawlers).

> [Brand direction above]
>
> Design a **1200×630 px social share card**. Dark slate `#0F172A` background
> with a subtle texture of faint spark dots in the lower-right, glowing amber.
> Left side: the word **"forge"** in a heavy geometric sans-serif, off-white
> `#F8FAFC`, lowercase, large. Directly beneath it, one line of tagline in a
> lighter weight, muted slate-grey: **"Run git hooks inside DDEV & Docker."**
> Right side: the orange anvil mark from asset #1, large, with a few bright
> sparks. Generous margins, nothing within 60px of any edge (safe zone). Clean,
> high-contrast, readable as a thumbnail.
>
> Output: exactly 1200×630 PNG.

---

## After generating

Drop the files here (VitePress serves `public/` at the site root under `base`):

```
website/public/logo.svg # or logo.png — update themeConfig.logo if PNG
website/public/favicon.ico
website/public/og-image.png # then point og:image/twitter:image at it
```

Then in `website/.vitepress/config.mts`, change the `og:image` / `twitter:image`
`content` from `logo.svg` to `og-image.png`, and add
`['meta', { name: 'twitter:card', content: 'summary_large_image' }]` (swap from
`summary`) so the 1200×630 card renders full-width in previews.
29 changes: 24 additions & 5 deletions website/.vitepress/config.mts
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
import { defineConfig } from 'vitepress'

const hostname = 'https://terrorsquad.github.io/forge/'

export default defineConfig({
title: 'forge',
description: 'Policy-driven git hook runner — fast, portable, no Node.js required.',
description:
'A single-binary git hook runner that runs your linters and formatters inside DDEV or Docker containers automatically. No Node.js. A Husky and lefthook alternative for containerized PHP, Go, and polyglot repos.',
base: '/forge/',
lastUpdated: true,
cleanUrls: true,
sitemap: { hostname },

head: [
['link', { rel: 'icon', href: '/forge/favicon.ico' }],
['meta', { name: 'keywords', content: 'git hooks, git hook runner, DDEV, Docker, pre-commit, commit-msg, Husky alternative, lefthook alternative, conventional commits, PHP, Go, monorepo' }],
['meta', { property: 'og:type', content: 'website' }],
['meta', { property: 'og:site_name', content: 'forge' }],
['meta', { property: 'og:title', content: 'forge — git hook runner for DDEV, Docker & any project' }],
['meta', { property: 'og:description', content: 'Run your linters and formatters inside DDEV or Docker containers automatically. Single Go binary, no Node.js. A Husky/lefthook alternative for containerized repos.' }],
['meta', { property: 'og:url', content: hostname }],
['meta', { property: 'og:image', content: hostname + 'og-image.png' }],
['meta', { property: 'og:image:width', content: '1200' }],
['meta', { property: 'og:image:height', content: '630' }],
['meta', { name: 'twitter:card', content: 'summary_large_image' }],
['meta', { name: 'twitter:image', content: hostname + 'og-image.png' }],
['meta', { name: 'twitter:title', content: 'forge — git hook runner for DDEV, Docker & any project' }],
['meta', { name: 'twitter:description', content: 'Run git hooks inside DDEV/Docker containers automatically. Single binary, no Node.js.' }],
],

themeConfig: {
logo: '/logo.svg',
logo: '/logo.png',

nav: [
{ text: 'Guide', link: '/guide/installation' },
{ text: 'Reference', link: '/reference/cli' },
{ text: 'Changelog', link: 'https://github.com/TerrorSquad/forge/blob/master/CHANGELOG.md' },
{ text: 'Changelog', link: 'https://github.com/TerrorSquad/forge/blob/main/CHANGELOG.md' },
{
text: 'GitHub',
link: 'https://github.com/TerrorSquad/forge',
Expand All @@ -36,7 +55,7 @@ export default defineConfig({
items: [
{ text: 'Configuration', link: '/guide/configuration' },
{ text: 'Hooks', link: '/guide/hooks' },
{ text: 'Backends (DDEV)', link: '/guide/backends' },
{ text: 'Backends (DDEV / Docker)', link: '/guide/backends' },
{ text: 'Workspace / Monorepo', link: '/guide/workspace' },
{ text: 'Commit-message Policy', link: '/guide/commit-policy' },
],
Expand Down Expand Up @@ -66,7 +85,7 @@ export default defineConfig({
},

editLink: {
pattern: 'https://github.com/TerrorSquad/forge/edit/master/website/:path',
pattern: 'https://github.com/TerrorSquad/forge/edit/main/website/:path',
text: 'Edit this page on GitHub',
},
},
Expand Down
8 changes: 4 additions & 4 deletions website/guide/backends.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Backends (DDEV)
# Backends (DDEV / Docker)

forge can execute tools either on the host machine or inside a DDEV container.
forge can execute tools on the host machine, inside a DDEV container, or inside any named Docker container.

## How it works

Expand All @@ -15,12 +15,12 @@ Environment variables (e.g., `PATH` expansions) are forwarded explicitly via `-e

## Auto-detection

forge automatically uses the DDEV backend when:
When `default_backend` is **not set**, forge automatically uses the DDEV backend when:

- `.ddev/config.yaml` exists in the repo root, **and**
- the container is running.

If the container is not running, forge falls back to the host backend and emits a warning.
If the container is not running, forge falls back to the host backend and emits a warning. Setting `default_backend = "host"` disables auto-detection entirely.

## Configuration

Expand Down
20 changes: 18 additions & 2 deletions website/guide/commit-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,27 @@ conventional_commits = true

## Policy fields

These apply on the **`commit-msg`** hook:

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `conventional_commits` | bool | `false` | Enforce [Conventional Commits](https://www.conventionalcommits.org/) format |
| `append_ticket_footer` | bool | `false` | Append `Closes: PRJ-123` from branch name |
| `allowed_types` | `[]string` | *(default set)* | Override the allowed Conventional Commits types (see below) |
| `append_ticket_footer` | bool | `false` | Append `Closes: PRJ-123` from the branch name |
| `footer_label` | string | `Closes` | Label used for the appended footer (e.g. `Refs`) |
| `require_ticket` | bool | `false` | Fail if the current branch has no ticket ID |
| `ticket_pattern` | string | `([A-Z]+-[0-9]+)` | Regex (with a capture group) used to extract the ticket ID from the branch name |
| `validate_branch_name` | bool | `false` | Fail if the branch name doesn't match `branch_pattern` |
| `branch_pattern` | string | — | Regex the branch name must match when `validate_branch_name = true` |
| `skipped_branches` | `[]string` | `[]` | Exact branch names to skip the policy on (e.g. `main`, `develop`) |

These apply on the **`prepare-commit-msg`** hook (enable `[hooks.prepare-commit-msg]`):

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `prepend_ticket` | bool | `false` | Prepend `PRJ-123: ` to the subject before the editor opens |
| `skip_if_present` | bool | `false` | Skip prepending if the ticket is already in the message |
| `skip_on_merge` | bool | `false` | Skip prepending on merge/squash commits |

## Conventional Commits

Expand All @@ -40,7 +56,7 @@ feat(auth): add OAuth2 support
Closes: PRJ-123
```

Branch naming convention: any branch containing `PRJ-123` or `PRJ_123`.
The ticket is extracted from the branch name using `ticket_pattern` (default `([A-Z]+-[0-9]+)`), so a branch like `feature/PRJ-123-oauth` yields `PRJ-123`. For GitHub issues, set `ticket_pattern = "(#[0-9]+)"`.

## Requiring a ticket

Expand Down
15 changes: 13 additions & 2 deletions website/guide/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,27 @@ Tool sections are executed in the order they are declared in `forge.toml`. There

```toml
[execution]
default_backend = "ddev" # "host" (default) or "ddev"
default_backend = "ddev" # omit to auto-detect DDEV; or "host" / a container name
parallel = true # run each hook's tools concurrently
cache = true # skip tools whose inputs haven't changed
tool_timeout = "60s" # default timeout per tool
```

See the [full reference](/reference/config#execution) for caching and timeout options.

## Config file path

forge loads the first repo config it finds:

| Priority | Source |
|----------|--------|
| 1 | `FORGE_CONFIG` env var |
| 1 | `FORGE_CONFIG` env var (path relative to repo root, or absolute) |
| 2 | `forge.toml` in repo root |

## Global user config

A user-level config at `~/.config/forge/config.toml` (override with `FORGE_GLOBAL_CONFIG`, respects `XDG_CONFIG_HOME`) is merged **underneath** the repo config — the repo's values always win. Use it for personal defaults like `[execution] default_backend` or a shared commit-message policy across all your repos.

## See also

- [Hooks](/guide/hooks)
Expand Down
12 changes: 9 additions & 3 deletions website/guide/hooks.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
# Hooks

forge supports three git hook entry points. Each maps directly to a standard git hook.
Each hook maps directly to a standard git hook. `forge install` writes a shim for every supported hook.

## Supported hooks

| Hook | Trigger |
|------|---------|
| `pre-commit` | Before a commit is created; receives staged files |
| `commit-msg` | After commit message is written; validates / mutates message |
| `pre-push` | Before a push; can run slower checks (tests, build) |
| `commit-msg` | After the commit message is written; validates / mutates the message |
| `prepare-commit-msg` | Before the editor opens; can pre-fill the message (e.g. ticket prefix) |
| `pre-push` | Before a push; good for slower checks (tests, build) |
| `post-commit` | After a commit completes; non-blocking |
| `post-merge` | After a merge (e.g. `git pull`); non-blocking |
| `post-rewrite` | After history is rewritten (`rebase`, `commit --amend`); non-blocking |

`pre-commit`, `pre-push`, and the `post-*` hooks run configured tools. `commit-msg` and `prepare-commit-msg` additionally apply the [commit-message policy](/guide/commit-policy).

## Enabling a hook

Expand Down
4 changes: 2 additions & 2 deletions website/guide/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ brew install forge-git
## curl installer

```sh
curl -fsSL https://raw.githubusercontent.com/TerrorSquad/forge/master/install.sh | sh
curl -fsSL https://raw.githubusercontent.com/TerrorSquad/forge/main/install.sh | sh
```

The script downloads the latest release binary for your OS/arch and installs it to `/usr/local/bin` (or `~/.local/bin` if `/usr/local/bin` is not writable).
Expand All @@ -36,7 +36,7 @@ forge version
```

```
forge v1.0.0 (abc1234, 2024-01-01)
forge v2.0.0 (commit: abc1234, built: 2024-01-01)
```

> **Note:** The Homebrew formula is named `forge-git` to avoid conflicts with an existing `forge` package. Install with `brew install forge-git` but the binary is named `forge`.
Expand Down
Loading
Loading