Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

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:

ConcernCurrent toolsRuntime
Commit message conventionscommitizen, commitlintNode.js
Version bumpingstandard-version, release-pleaseNode.js
Changelog generationgit-cliff, conventional-changelogRust / Node.js
Git hooks managementpre-commit, husky, lefthookPython / 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

  1. One binary, four concerns. Commits, versions, changelogs, and hooks ship as a single tool. No Node.js runtime, no Python, no framework dependencies.

  2. Native hooks only. No pre-commit.com framework. Git hooks are plain shell commands in .githooks/*.hooks files, managed natively by git std hooks.

  3. Config is TOML. .git-std.toml is the single config file for versioning, changelog, and commit conventions. Hooks config lives in the .githooks/*.hooks files themselves — plain text, not TOML or YAML.

  4. Install via curl. Precompiled static binaries per OS/arch. Not distributed through cargo install — avoids requiring the Rust toolchain on developer machines.

  5. Single responsibility. git-std owns everything between git commit and git tag — the developer’s git workflow. It does not touch repo structure, compliance, formatting, or linting.

  6. Zero-config sensible defaults. Every subcommand works without .git-std.toml. Missing config means conventional commit standard types, semver, v tag prefix, and default changelog sections.

1.3 Scope

git-std covers four concerns:

ConcernSubcommand
Conventional commit creationgit std commit
Commit message validationgit std check
Version bump + changeloggit std bump, git std changelog
Git hooks managementgit 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

FilePurpose
.git-std.tomlVersioning scheme, commit types, scopes, changelog sections
.githooks/*.hooksHook command definitions (one file per git hook)
.githooks/<hook>Shim scripts generated by git std hooks install
Cargo.tomlVersion 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:

CrateRole
git-stdCLI binary — orchestrates I/O, git, config, dispatch
standard-commitConventional commit parsing, linting, formatting
standard-versionSemantic version bump calculation, version file detection and update (with file I/O)
standard-changelogChangelog generation from conventional commits
standard-githooksHook 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:

CratePurpose
clapCLI argument parsing with subcommand dispatch
inquireInteractive terminal prompts (type/scope/description selection)
yansiTerminal 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)
semverSemantic 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:

OSArchitectureTarget triple
Linuxx86_64x86_64-unknown-linux-musl
Linuxaarch64aarch64-unknown-linux-musl
macOSx86_64x86_64-apple-darwin
macOSaarch64 (Apple Silicon)aarch64-apple-darwin
Windowsx86_64x86_64-pc-windows-msvc

Install script at https://raw.githubusercontent.com/driftsys/git-std/main/install.sh:

  1. Detects OS and architecture
  2. Downloads the matching binary from the latest GitHub Release
  3. Installs to ~/.local/bin/git-std (Linux/macOS) or %LOCALAPPDATA%\bin\git-std.exe (Windows)
  4. 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:

  1. If .git-std.toml exists, read types for the type list and scopes for scope suggestions. Otherwise, use the conventional commit standard types.
  2. Prompt for type (required), scope (optional), subject (required), body (optional), breaking change footer (optional).
  3. Assemble the message: <type>(<scope>): <subject>\n\n<body>\n\nBREAKING CHANGE: <description>.
  4. Validate the assembled message against the conventional commit spec (same rules as git std check).
  5. Run git commit -m "<message>" with any passthrough flags (--sign, --amend, --all).

Flags:

FlagDescription
--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.
--breakingAdd BREAKING CHANGE footer (prompts for description)
--dry-runPrint the assembled message to stdout, do not commit
--amendPass --amend to git commit
--sign / -SPass --gpg-sign to git commit
--all / -aPass --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:

  1. Message matches ^<type>(\(<scope>\))?!?: .+ where type is alphanumeric lowercase.
  2. Subject line (first line) does not exceed 100 characters.
  3. If --strict flag or strict = true in config: type must be in the known types list (from .git-std.toml or defaults). Scope must be in the known scopes list if scopes is defined.
  4. Body, if present, is separated from subject by a blank line.
  5. Footer tokens (BREAKING CHANGE:, Refs:, etc.) follow the conventional commit footer spec.

Input modes:

ModeUsage
Positional argumentgit std check "feat: add feature"
--file <path>git std check --file .git/COMMIT_EDITMSG
--range <range>git std check --range main..HEAD
stdinecho "feat: ok" | git std check -

Flags:

FlagDescription
--file <path>Read message from file
--range <range>Validate all commits in a git revision range
--strictEnforce: 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:

  1. Find the latest version tag matching <tag_prefix><semver> (default v*).
  2. Collect all commits between that tag and HEAD.
  3. Parse each commit as a conventional commit. Non-conforming commits are ignored (not an error).
  4. Apply bump rules inferred from scheme: if any commit has a BREAKING CHANGE footer or ! after the type, bump major. Otherwise, the highest bump wins (minor > patch).
  5. If no bump-worthy commits exist, print a message and exit 0 (not an error).
  6. Compute the new version string.
  7. 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.
  8. Sync Cargo.lock via cargo update --workspace (only when Cargo.toml was updated).
  9. Generate changelog section for this release via standard-changelog.
  10. Prepend the section to CHANGELOG.md.
  11. Create commit: chore(release): <version>. All updated version files are included in the release commit.
  12. Create annotated tag: <tag_prefix><version>.

Flags:

FlagDescription
--dry-runPrint 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-releaseUse current version for initial changelog. No bump.
--no-tagUpdate files and commit, skip tag creation
--no-commitUpdate files only, no commit or tag
--signGPG-sign the release commit and annotated tag
--skip-changelogBump 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:

TokenDescriptionExample
YYYYFull year2026
YYShort year26
0MZero-padded month03
MMMonth (no padding)3
WWISO week number11
DDDay of month13
PATCHAuto-incrementing patch counter, resets each period0, 1, 2
DPDay of week (1=Mon–7=Sun) concatenated with patch counter30, 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.

FileEcosystemEngineDetection
Cargo.tomlRustTOML (toml)already implemented
package.jsonNode / TypeScriptJSONpackage.json at root
deno.json / deno.jsoncDenoJSON/JSONCdeno.json or deno.jsonc at root
pyproject.tomlPythonTOML (toml)pyproject.toml at root with [project] table
pubspec.yamlFlutter / Darttext (line-level)pubspec.yaml at root
gradle.propertiesAndroid / Gradletext (key=value)gradle.properties at root with VERSION_NAME key
VERSIONanytext (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 / JSONCserde_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.propertiesVERSION_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:

  • path is relative to repo root.
  • regex uses RE2 syntax (Rust regex crate — 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:

  1. Read section mapping from .git-std.toml [changelog.sections]. Falls back to defaults if absent.
  2. Parse commits in the requested range as conventional commits.
  3. Group commits by type → changelog section. Hidden types excluded.
  4. Render as CommonMark with reference-style links.
  5. Write to the output file (prepend by default) or stdout.

Flags:

FlagDescription
--fullRegenerate the entire changelog from the first commit
--range <range>Generate changelog for a tag range (e.g. v1.0..v2.0)
--stdoutPrint 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:

  1. Configures git: runs git config core.hooksPath .githooks.
  2. Creates directory: ensures .githooks/ exists.
  3. Writes shims: for each <hook>.hooks file, 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:

  1. Read .githooks/<hook>.hooks, skip blank lines and # comments.
  2. Parse prefix, command, and optional trailing glob per line.
  3. If glob present: check staged files (pre-commit) or tracked files (other hooks) against glob. Skip silently if no match.
  4. Execute command via sh -c.
  5. 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:

PrefixNameOn non-zero exit
(none)DefaultUses the hook’s default mode
!Fail fastAbort immediately
?AdvisoryReport as warning, never fail

Default mode per hook:

HookDefaultRationale
pre-commitCollectShow all issues at once
pre-pushFail fastDon’t push broken code
commit-msgFail fastReject bad messages immediately
(other)CollectSafe 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

FlagDescription
--help / -hPrint help
--version / -VPrint git-std version
--color <when>auto (default), always, never
--quiet / -qSuppress 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:

FieldDefault
schemesemver
typesfeat, fix, docs, style, refactor, perf, test, chore, ci, build
scopesNone (no scope validation)
strictfalse
versioning.tag_prefixv
versioning.prerelease_tagrc
versioning.calver_formatYYYY.MM.PATCH
changelog.hiddenchore, ci, build, style, test
changelog.sectionsSensible defaults for each type
version_files[] (empty — built-in files are always auto-detected)

Inferred (not configurable):

ConcernHow it’s resolved
Bump rulesInferred from scheme (semver/calver/patch)
Version filesAuto-detected (see §2.3.1); extensible via [[version_files]]
URLsInferred from git remote get-url origin
Changelog outputAlways CHANGELOG.md
Release commitAlways 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 toolWhat git-std replaces
commitizen (cz)Interactive commit builder
commitlintCommit message validation
standard-version / commit-and-tag-versionVersion bump + changelog (.git-std.toml is the config file)
release-pleaseVersion bump + changelog (git-std is local-only, no PR creation)
git-cliffChangelog generation (uses git_cliff_core as a library)
pre-commit (framework)Hook management (native hooks, no framework)
huskyHook management (no Node.js dependency)
lefthookHook management (simpler syntax, glob concept kept)

Appendix C — Open Design Questions

  1. cliff.toml coexistence. .git-std.toml is primary config. cliff.toml is optional override for advanced git-cliff template customization. Right layering?

  2. Scope auto-discovery. Resolved. Three-way: not set (default, no validation), scopes = "auto" (discover from workspace layout), or scopes = ["auth", "api"] (explicit allowlist).

  3. Monorepo version sync. Post-bump, validate that workspace-inherited versions resolve correctly? Leaning: yes, warn on mismatch.

  4. Hook parallelism. Sequential by default. --parallel flag deferred to a future version.