git-std — Specification
A single Rust binary for conventional commits, version bumping, changelog generation, and git hooks management.
Repository: driftsys/git-std
Binary: git-std (invoked as git std via git’s subcommand discovery)
Status: Draft
Part 1 — Architecture
1.1 Problem Statement
Modern git workflows require four distinct concerns currently served by separate tools across multiple runtimes:
| Concern | Current tools | Runtime |
|---|---|---|
| Commit message conventions | commitizen, commitlint | Node.js |
| Version bumping | standard-version, release-please | Node.js |
| Changelog generation | git-cliff, conventional-changelog | Rust / Node.js |
| Git hooks management | pre-commit, husky, lefthook | Python / Node.js / Go |
Installing four tools across three runtimes to enforce
git conventions is excessive. git-std consolidates all
four into one statically-linked binary with zero runtime
dependencies.
1.2 Design Principles
-
One binary, four concerns. Commits, versions, changelogs, and hooks ship as a single tool. No Node.js runtime, no Python, no framework dependencies.
-
Native hooks only. No pre-commit.com framework. Git hooks are plain shell commands in
.githooks/*.hooksfiles, managed natively bygit std hooks. -
Config is TOML.
.git-std.tomlis the single config file for versioning, changelog, and commit conventions. Hooks config lives in the.githooks/*.hooksfiles themselves — plain text, not TOML or YAML. -
Install via curl. Precompiled static binaries per OS/arch. Not distributed through
cargo install— avoids requiring the Rust toolchain on developer machines. -
Single responsibility. git-std owns everything between
git commitandgit tag— the developer’s git workflow. It does not touch repo structure, compliance, formatting, or linting. -
Zero-config sensible defaults. Every subcommand works without
.git-std.toml. Missing config means conventional commit standard types, semver,vtag prefix, and default changelog sections.
1.3 Scope
git-std covers four concerns:
| Concern | Subcommand |
|---|---|
| Conventional commit creation | git std commit |
| Commit message validation | git std check |
| Version bump + changelog | git std bump, git std changelog |
| Git hooks management | git std hooks install, git std hooks run, git std hooks list |
Out of scope: repo scaffolding, directory structure compliance, formatting, linting, CI pipeline generation, dependency management.
1.4 Configuration Files
| File | Purpose |
|---|---|
.git-std.toml | Versioning scheme, commit types, scopes, changelog sections |
.githooks/*.hooks | Hook command definitions (one file per git hook) |
.githooks/<hook> | Shim scripts generated by git std hooks install |
Cargo.toml | Version file — git-std reads/writes the version field only |
git-std auto-detects version files and reads/writes only
the version field during bump. See §2.3.1 for the full
list of built-in version files.
1.5 Implementation
Language: Rust
Workspace crates:
| Crate | Role |
|---|---|
git-std | CLI binary — orchestrates I/O, git, config, dispatch |
standard-commit | Conventional commit parsing, linting, formatting |
standard-version | Semantic version bump calculation, version file detection and update (with file I/O) |
standard-changelog | Changelog generation from conventional commits |
standard-githooks | Hook file format parsing, shim generation |
Library crates are pure — no git2, no I/O, no terminal
output — except standard-version, which performs file
I/O for version file detection and updates.
Key dependencies:
| Crate | Purpose |
|---|---|
clap | CLI argument parsing with subcommand dispatch |
inquire | Interactive terminal prompts (type/scope/description selection) |
yansi | Terminal colours and --color flag support |
toml | .git-std.toml reading and Cargo.toml version updates |
git2 (libgit2) | Git operations (log, tag, commit) — avoids shelling out to git (except for GPG signing) |
semver | Semantic version parsing and bumping |
Binary size target: ~5-8 MB statically linked
(lto = true, strip = true, codegen-units = 1).
Static linking: musl on Linux for
glibc-independent distribution. Separate x86_64 and
aarch64 builds per platform.
1.6 Distribution
Precompiled binaries published as GitHub Release assets:
| OS | Architecture | Target triple |
|---|---|---|
| Linux | x86_64 | x86_64-unknown-linux-musl |
| Linux | aarch64 | aarch64-unknown-linux-musl |
| macOS | x86_64 | x86_64-apple-darwin |
| macOS | aarch64 (Apple Silicon) | aarch64-apple-darwin |
| Windows | x86_64 | x86_64-pc-windows-msvc |
Install script at
https://raw.githubusercontent.com/driftsys/git-std/main/install.sh:
- Detects OS and architecture
- Downloads the matching binary from the latest GitHub Release
- Installs to
~/.local/bin/git-std(Linux/macOS) or%LOCALAPPDATA%\bin\git-std.exe(Windows) - Verifies with
git std --version
Git discovers git-std as a subcommand automatically
when it’s on $PATH — running git std invokes the
git-std binary.
1.7 Dogfooding
The driftsys/git-std repository uses git-std for its
own releases. CI builds via cross-compilation (GitHub
Actions matrix), creates a GitHub Release with SHA-256
checksums, and publishes the install script. The
.git-std.toml in the repo is the reference
configuration.
Part 2 — Feature Specification
2.1 git std commit
Interactive conventional commit builder. Creates a
well-formed conventional commit message and runs
git commit.
Behaviour:
- If
.git-std.tomlexists, readtypesfor the type list andscopesfor scope suggestions. Otherwise, use the conventional commit standard types. - Prompt for type (required), scope (optional), subject (required), body (optional), breaking change footer (optional).
- Assemble the message:
<type>(<scope>): <subject>\n\n<body>\n\nBREAKING CHANGE: <description>. - Validate the assembled message against the
conventional commit spec (same rules as
git std check). - Run
git commit -m "<message>"with any passthrough flags (--sign,--amend,--all).
Flags:
| Flag | Description |
|---|---|
--type <type> | Pre-fill type, skip type prompt |
--scope <scope> | Pre-fill scope, skip scope prompt |
--message <msg> | Non-interactive mode, full message provided. Validated before committing. |
--breaking | Add BREAKING CHANGE footer (prompts for description) |
--dry-run | Print the assembled message to stdout, do not commit |
--amend | Pass --amend to git commit |
--sign / -S | Pass --gpg-sign to git commit |
--all / -a | Pass --all to git commit (stage tracked changes) |
Exit codes: 0 = committed, 1 = validation failed
or git error, 2 = usage error.
Example — interactive:
$ git std commit
? Type: feat
? Scope: auth
? Subject: add OAuth2 PKCE flow
? Body: (empty)
? Breaking: (none)
→ feat(auth): add OAuth2 PKCE flow
Example — non-interactive:
git std commit -a --type fix --scope auth -m "fix(auth): handle expired refresh tokens"
2.2 git std check
Validate one or more commit messages against the conventional commit specification.
Validation rules:
- Message matches
^<type>(\(<scope>\))?!?: .+where type is alphanumeric lowercase. - Subject line (first line) does not exceed 100 characters.
- If
--strictflag orstrict = truein config: type must be in the known types list (from.git-std.tomlor defaults). Scope must be in the known scopes list ifscopesis defined. - Body, if present, is separated from subject by a blank line.
- Footer tokens (
BREAKING CHANGE:,Refs:, etc.) follow the conventional commit footer spec.
Input modes:
| Mode | Usage |
|---|---|
| Positional argument | git std check "feat: add feature" |
--file <path> | git std check --file .git/COMMIT_EDITMSG |
--range <range> | git std check --range main..HEAD |
| stdin | echo "feat: ok" | git std check - |
Flags:
| Flag | Description |
|---|---|
--file <path> | Read message from file |
--range <range> | Validate all commits in a git revision range |
--strict | Enforce: type must be known, scope must be known (if defined), scope required |
--format <fmt> | Output format: text (default), json |
Exit codes: 0 = all valid, 1 = one or more invalid.
Output on failure:
$ git std check "added new feature"
✗ invalid: missing type prefix
Expected: <type>(<scope>): <description>
Got: added new feature
Output with --range:
$ git std check --range main..HEAD
✓ feat(auth): add OAuth2 PKCE flow
✓ fix(auth): handle expired refresh tokens
✗ updated readme
→ missing type prefix
2/3 valid
2.3 git std bump
Calculate the next version from conventional commits since the last tag, update version files, generate changelog, commit, and tag.
Algorithm:
- Find the latest version tag matching
<tag_prefix><semver>(defaultv*). - Collect all commits between that tag and
HEAD. - Parse each commit as a conventional commit. Non-conforming commits are ignored (not an error).
- Apply bump rules inferred from
scheme: if any commit has aBREAKING CHANGEfooter or!after the type, bump major. Otherwise, the highest bump wins (minor>patch). - If no bump-worthy commits exist, print a message and exit
0(not an error). - Compute the new version string.
- Update all version files:
a. Auto-detected built-in files (see §2.3.1).
b. Custom files from
.git-std.toml[[version_files]](see §2.3.2). c. Report each file updated in the output. - Sync
Cargo.lockviacargo update --workspace(only whenCargo.tomlwas updated). - Generate changelog section for this release via
standard-changelog. - Prepend the section to
CHANGELOG.md. - Create commit:
chore(release): <version>. All updated version files are included in the release commit. - Create annotated tag:
<tag_prefix><version>.
Flags:
| Flag | Description |
|---|---|
--dry-run | Print the full plan without writing anything |
--prerelease [tag] | Bump as pre-release (e.g., 2.0.0-rc.1). Default tag from [versioning] prerelease_tag. |
--release-as <version> | Force a specific version, skip calculation |
--first-release | Use current version for initial changelog. No bump. |
--no-tag | Update files and commit, skip tag creation |
--no-commit | Update files only, no commit or tag |
--sign | GPG-sign the release commit and annotated tag |
--skip-changelog | Bump version files without changelog generation |
Exit codes: 0 = success (or no bump needed), 1 = error.
Output:
$ git std bump
Analysing commits since v1.4.2...
3 feat, 2 fix, 1 BREAKING CHANGE
1.4.2 → 2.0.0 (major — breaking change detected)
Updated:
Cargo.toml 1.4.2 → 2.0.0
package.json 1.4.2 → 2.0.0
gradle.properties 1.4.2 → 2.0.0 (VERSION_CODE: 42 → 43)
Changelog:
CHANGELOG.md prepended v2.0.0 section
Committed: chore(release): 2.0.0
Tagged: v2.0.0
Push with: git push --follow-tags
Version file resolution: git-std auto-detects
built-in version files at the repository root (see
§2.3.1). For Cargo.toml workspace manifests, it
finds the binary crate member. Additional files can
be declared via [[version_files]] in
.git-std.toml (see §2.3.2).
Calver support: when scheme = "calver", date-based
segments are computed from the current date. Default
format YYYY.MM.PATCH. The PATCH counter increments
if the date segments match the previous version, and
resets to 0 when they change.
Calver format tokens:
| Token | Description | Example |
|---|---|---|
YYYY | Full year | 2026 |
YY | Short year | 26 |
0M | Zero-padded month | 03 |
MM | Month (no padding) | 3 |
WW | ISO week number | 11 |
DD | Day of month | 13 |
PATCH | Auto-incrementing patch counter, resets each period | 0, 1, 2 |
DP | Day of week (1=Mon–7=Sun) concatenated with patch counter | 30, 31, 32 |
Common formats: YYYY.MM.PATCH (monthly),
YYYY.0M.PATCH (zero-padded month), YY.WW.DP
(weekly with day-of-week), YYYY.MM.DD.PATCH (daily).
2.3.1 Built-in Version Files
Auto-detected from root markers. No config needed.
| File | Ecosystem | Engine | Detection |
|---|---|---|---|
Cargo.toml | Rust | TOML (toml) | already implemented |
package.json | Node / TypeScript | JSON | package.json at root |
deno.json / deno.jsonc | Deno | JSON/JSONC | deno.json or deno.jsonc at root |
pyproject.toml | Python | TOML (toml) | pyproject.toml at root with [project] table |
pubspec.yaml | Flutter / Dart | text (line-level) | pubspec.yaml at root |
gradle.properties | Android / Gradle | text (key=value) | gradle.properties at root with VERSION_NAME key |
VERSION | any | text (overwrite) | VERSION file at root |
Detection is best-effort: if the file exists and
contains a recognisable version field, update it. If
the file exists but has no version field (e.g. a
package.json without "version"), skip silently.
Never create files that don’t already exist.
Engine details:
TOML — reuse the existing toml crate.
Cargo.toml already works. pyproject.toml reads
[project] version. Preserves comments and
formatting.
JSON / JSONC — serde_json for package.json.
JSONC support for deno.jsonc needs
comment-preserving handling (line-level regex on the
"version" field, or a JSONC-aware crate).
Text (line-level) — pubspec.yaml: match
^version:\s*(.+) and replace the version part.
gradle.properties: match ^VERSION_NAME=(.+) and
replace. VERSION: overwrite entire file content
with the version string + newline.
gradle.properties — VERSION_CODE:
Android projects use two version identifiers:
VERSION_NAME (semver string) and VERSION_CODE
(monotonic integer). git std bump updates
VERSION_NAME to the new version. VERSION_CODE is
incremented by 1 on every bump. If VERSION_CODE is
not present, it is not added.
pubspec.yaml — build number:
Flutter’s version field supports a build number
suffix: 1.2.3+42. If the existing value has a +N
suffix, increment N by 1 on every bump. If no
suffix is present, leave it without one.
2.3.2 Custom Version Files
Users can declare additional files in .git-std.toml
using regex. The first capture group is the version
string to replace.
[[version_files]]
path = "pom.xml"
regex = '<version>(.*)</version>'
[[version_files]]
path = "CMakeLists.txt"
regex = 'project\(myapp VERSION ([^\)]+)'
[[version_files]]
path = "Directory.Build.props"
regex = '<Version>(.*)</Version>'
Rules:
pathis relative to repo root.regexuses RE2 syntax (Rustregexcrate — linear time, no backtracking).- First capture group = version string to replace. No capture group = error at config parse time.
- If the file doesn’t exist, warn and skip (not an error).
- If the regex doesn’t match, warn and skip.
- Multiple
[[version_files]]entries are supported.
2.4 git std changelog
Generate or update the changelog from git commit history
using standard-changelog.
Behaviour:
- Read section mapping from
.git-std.toml[changelog.sections]. Falls back to defaults if absent. - Parse commits in the requested range as conventional commits.
- Group commits by type → changelog section. Hidden types excluded.
- Render as CommonMark with reference-style links.
- Write to the output file (prepend by default) or stdout.
Flags:
| Flag | Description |
|---|---|
--full | Regenerate the entire changelog from the first commit |
--range <range> | Generate changelog for a tag range (e.g. v1.0..v2.0) |
--stdout | Print to stdout instead of file |
--output <file> | Write to file (default: CHANGELOG.md) |
Without --full or --range, generates an incremental
changelog (unreleased commits since the last tag) and
prepends to the output file. --range and --full are
mutually exclusive.
2.5 git std hooks
Manage and execute git hooks defined in .githooks/*.hooks files.
2.5.1 git std hooks install
Sets up the hooks directory and generates shim scripts. Performs three actions:
- Configures git: runs
git config core.hooksPath .githooks. - Creates directory: ensures
.githooks/exists. - Writes shims: for each
<hook>.hooksfile, writes a.githooks/<hook>shim.
Each shim delegates to git std hooks run:
#!/bin/bash
exec git std hooks run <hook> -- "$@"
Idempotent — re-running overwrites existing shims.
User-created hook scripts without a matching .hooks
file are not touched.
Output:
$ git std hooks install
✓ core.hooksPath → .githooks
✓ .githooks/pre-commit → git std hooks run pre-commit
✓ .githooks/pre-push → git std hooks run pre-push
✓ .githooks/commit-msg → git std hooks run commit-msg
2.5.2 git std hooks run <hook>
Execute all commands in .githooks/<hook>.hooks.
Execution model:
- Read
.githooks/<hook>.hooks, skip blank lines and#comments. - Parse prefix, command, and optional trailing glob per line.
- If glob present: check staged files (pre-commit) or tracked files (other hooks) against glob. Skip silently if no match.
- Execute command via
sh -c. - Apply prefix rule to the exit code.
Arguments after -- are passed to each command. The
{msg} token is substituted with the commit message
file path.
Prefix rules:
| Prefix | Name | On non-zero exit |
|---|---|---|
| (none) | Default | Uses the hook’s default mode |
! | Fail fast | Abort immediately |
? | Advisory | Report as warning, never fail |
Default mode per hook:
| Hook | Default | Rationale |
|---|---|---|
pre-commit | Collect | Show all issues at once |
pre-push | Fail fast | Don’t push broken code |
commit-msg | Fail fast | Reject bad messages immediately |
| (other) | Collect | Safe default |
Exit code: 0 if all non-advisory commands passed, 1 otherwise.
Output (collect mode):
$ git std hooks run pre-commit
✓ dprint check
✗ shellcheck scripts/*.sh (exit 1)
✓ cargo clippy --workspace (*.rs)
⚠ detekt --input modules/ (advisory, 4 findings)
1 failed, 1 advisory warning
2.5.3 git std hooks list
Display all configured hooks and their commands:
$ git std hooks list
pre-commit (collect mode):
dprint check
shellcheck scripts/*.sh
cargo clippy --workspace -- -D warnings *.rs
? detekt --input modules/ *.kt
pre-push (fail-fast mode):
! cargo build --workspace
! cargo test --workspace
commit-msg (fail-fast mode):
! git std check --file {msg}
2.6 Hooks File Format
.githooks/<hook-name>.hooks — one command per line, optional prefix and glob.
# Comment
[prefix]command [arguments] [glob]
Prefixes: (none) = hook default, ! = fail fast, ? = advisory.
Globs (optional, end of line): restrict command to matching staged/tracked files. Git pathspec syntax. No match → skip silently.
Substitutions: {msg} → commit message file path.
Section markers: comments can be used to organise commands by concern. git-std ignores comment lines — they’re purely for human readability.
Example .githooks/pre-commit.hooks:
# ── Formatting ────────────────────────────
dprint check
prettier --check "**/*.md"
# ── Rust ──────────────────────────────────
cargo clippy --workspace -- -D warnings *.rs
cargo test --workspace --lib *.rs
# ── Android ───────────────────────────────
? detekt --input modules/ *.kt
Example .githooks/commit-msg.hooks:
! git std check --file {msg}
2.7 git std self-update
Fetch the latest release and replace the current binary:
$ git std self-update
Current: v0.2.0
Latest: v0.3.1
Installed: ~/.local/bin/git-std
2.8 Global Flags
| Flag | Description |
|---|---|
--help / -h | Print help |
--version / -V | Print git-std version |
--color <when> | auto (default), always, never |
--quiet / -q | Suppress non-error output |
Part 3 — Configuration Reference
3.1 .git-std.toml
TOML format. Optional — all fields have sensible defaults.
# ── Project ───────────────────────────────────────────────────────
scheme = "semver" # semver | calver | patch
types = ["feat", "fix", "docs", "style",
"refactor", "perf", "test",
"chore", "ci", "build"]
scopes = ["auth", "api", "ci", "deps"] # "auto" | string[] | omit
strict = true # enforce types/scopes without --strict flag
# ── Versioning ────────────────────────────────────────────────────
[versioning]
tag_prefix = "v" # git tag prefix
prerelease_tag = "rc" # default pre-release id
calver_format = "YYYY.MM.PATCH" # only used when scheme = "calver"
# ── Changelog ─────────────────────────────────────────────────────
[changelog]
hidden = ["chore", "ci", "build", "style", "test"]
[changelog.sections]
feat = "Features"
fix = "Bug Fixes"
perf = "Performance"
refactor = "Refactoring"
docs = "Documentation"
# ── Custom version files ─────────────────────────────────
[[version_files]]
path = "pom.xml"
regex = '<version>(.*)</version>'
Defaults when .git-std.toml is absent:
| Field | Default |
|---|---|
scheme | semver |
types | feat, fix, docs, style, refactor, perf, test, chore, ci, build |
scopes | None (no scope validation) |
strict | false |
versioning.tag_prefix | v |
versioning.prerelease_tag | rc |
versioning.calver_format | YYYY.MM.PATCH |
changelog.hidden | chore, ci, build, style, test |
changelog.sections | Sensible defaults for each type |
version_files | [] (empty — built-in files are always auto-detected) |
Inferred (not configurable):
| Concern | How it’s resolved |
|---|---|
| Bump rules | Inferred from scheme (semver/calver/patch) |
| Version files | Auto-detected (see §2.3.1); extensible via [[version_files]] |
| URLs | Inferred from git remote get-url origin |
| Changelog output | Always CHANGELOG.md |
| Release commit | Always chore(release): <version> |
Part 4 — Usage Manual
4.1 Installation
curl -fsSL https://raw.githubusercontent.com/driftsys/git-std/main/install.sh | bash
git std --version
After installing, run git std hooks install in any
repo that has .githooks/*.hooks files.
4.2 Making Commits
Interactive:
git add .
git std commit
Quick (non-interactive):
git std commit -a --type fix --scope auth -m "fix(auth): handle expired tokens"
Preview without committing:
git std commit --dry-run
4.3 Validating Commits
# Single message
git std check "feat: add feature"
# All commits on your branch
git std check --range main..HEAD
# Strict mode (enforce known types and scopes)
git std check --strict --range main..HEAD
4.4 Releasing
Preview:
git std bump --dry-run
Execute:
git std bump
git push --follow-tags
Pre-release:
git std bump --prerelease # → 2.0.0-rc.1
git std bump --prerelease # → 2.0.0-rc.2
git std bump # → 2.0.0
Force a version:
git std bump --release-as 3.0.0
4.5 Changelog
# Preview unreleased changes
git std changelog --stdout
# Regenerate full history
git std changelog --full
4.6 Hooks
View hooks:
git std hooks list
Run manually (debugging):
git std hooks run pre-commit
Regenerate shims after editing .hooks files:
git std hooks install
Add a custom command: edit .githooks/pre-commit.hooks and add a line.
4.7 CI Integration
# GitLab CI
lint:commits:
stage: build
script:
- git std check --range $CI_MERGE_REQUEST_DIFF_BASE_SHA..HEAD
# GitHub Actions
- name: Validate commits
run: git std check --range ${{ github.event.pull_request.base.sha }}..${{ github.sha }}
JSON output for scripting:
git std check --range main..HEAD --format json
Appendix A — Conventional Commit Grammar
<message> ::= <header> [<blank-line> <body>] [<blank-line> <footer>+]
<header> ::= <type> ["(" <scope> ")"] ["!"] ":" " " <subject>
<type> ::= [a-z]+
<scope> ::= [a-zA-Z0-9_/.-]+
<subject> ::= .+ (max 100 chars for header line)
<footer> ::= <token> ":" " " <value>
| "BREAKING CHANGE" ":" " " <value>
Appendix B — Existing Tool Comparison
| Existing tool | What git-std replaces |
|---|---|
| commitizen (cz) | Interactive commit builder |
| commitlint | Commit message validation |
| standard-version / commit-and-tag-version | Version bump + changelog (.git-std.toml is the config file) |
| release-please | Version bump + changelog (git-std is local-only, no PR creation) |
| git-cliff | Changelog generation (uses git_cliff_core as a library) |
| pre-commit (framework) | Hook management (native hooks, no framework) |
| husky | Hook management (no Node.js dependency) |
| lefthook | Hook management (simpler syntax, glob concept kept) |
Appendix C — Open Design Questions
-
cliff.tomlcoexistence..git-std.tomlis primary config.cliff.tomlis optional override for advanced git-cliff template customization. Right layering? -
Scope auto-discovery.Resolved. Three-way: not set (default, no validation),scopes = "auto"(discover from workspace layout), orscopes = ["auth", "api"](explicit allowlist). -
Monorepo version sync. Post-bump, validate that workspace-inherited versions resolve correctly? Leaning: yes, warn on mismatch.
-
Hook parallelism. Sequential by default.
--parallelflag deferred to a future version.