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 hook.

  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 six concerns:

ConcernSubcommand
Conventional commit creationgit std commit
Commit message validationgit std lint
Version bump + changeloggit std bump, git std changelog
Maintainer setupgit std init
Git hooks managementgit std hook run, git std hook list
Post-clone bootstrapgit std bootstrap
Shell completionsgit std --completions <shell>

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 hook 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 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
clap_completeShell completion script generation (bash, zsh, fish)
inquireInteractive terminal prompts (type/scope/description selection)
yansiTerminal colours and --color flag support
toml.git-std.toml reading and Cargo.toml version updates
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), trailer footers (optional).
  3. Assemble the message: <type>(<scope>): <subject>\n\n<body>\n\n<trailers>.
  4. Validate the assembled message against the conventional commit spec (same rules as git std lint).
  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.
--body <text>Commit body paragraph (extended description), separated by blank line
--breakingAdd BREAKING CHANGE footer (prompts for description)
--footer <text>Add a trailer footer (repeatable, e.g. Co-authored-by: Name <email>)
--signoff / -sAdd Signed-off-by trailer from git user.name / user.email
--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)
? Footer:     (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"

# With trailers
git std commit --type feat -m "add login" --footer "Co-authored-by: Alice <a@b.com>"
git std commit --type fix -m "fix crash" -s
git std commit --type feat -m "new api" \
  --footer "Co-authored-by: Alice <a@b.com>" \
  --footer "Reviewed-by: Carol <c@d.com>"

# With commit body (subject ≤50 chars, body wrapped at 72 chars)
git std commit --type fix --scope cache \
  -m "prevent race condition in invalidation" \
  --body "The cache invalidation routine was checking stale entries
after acquiring the lock, creating a window where two threads
could invalidate the same entry. Wrap the check-and-clear in a
single lock acquisition instead of splitting into two operations."

Commit message guidelines:

  • Subject line: imperative mood, ≤50 characters, no period
  • Body (optional): explain what changed and why, not how — the diff shows that
  • Body wrapping: 72 characters per line for readability
  • Body length: 2–5 sentences typical

2.2 git std lint

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.
  6. In --range mode: process commits (merges, reverts, fixups, squashes, initial commits) are automatically skipped and excluded from the valid/total count.

Input modes:

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

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 lint "added new feature"
✗ invalid: missing type prefix
  Expected: <type>(<scope>): <description>
  Got:      added new feature

Output with --range:

Process commits (merges, reverts, fixups) are automatically skipped and not counted as failures.

$ git std lint --range main..HEAD
  ✓ feat(auth): add OAuth2 PKCE flow
  ✓ fix(auth): handle expired refresh tokens
  ~ abc1234 Merge pull request #42 from owner/branch
  ✗ updated readme
    → missing type prefix

2/3 valid  (1 skipped)

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. The highest bump level wins (major > minor > patch):

    • BREAKING CHANGE footer or ! suffix → major
    • feat → minor
    • fix / perf / revert → patch

    Pre-1.0 convention (major == 0): bump levels are downshifted following the Rust/Cargo convention:

    Commit type>= 1.0.0< 1.0.0
    Breaking changemajorminor
    Featureminorpatch
    Fixpatchpatch

    Example: 0.10.2 + breaking → 0.11.0, 0.10.2 + feat → 0.10.3. To force a 1.0.0 release: git std bump --release-as 1.0.0.

  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 ecosystem lock files. Only lock files that already exist on disk are synced. Missing tools emit a warning but never abort the bump. The version file name is matched with ends_with, so custom [[version_files]] entries pointing to subdirectory paths (e.g. crates/my-crate/Cargo.toml) are detected correctly.

    Version fileLock fileSync command
    Cargo.tomlCargo.lockcargo update --workspace
    package.jsonpackage-lock.jsonnpm install --package-lock-only
    package.jsonyarn.lockyarn install --mode update-lockfile
    package.jsonpnpm-lock.yamlpnpm install --lockfile-only
    deno.jsondeno.lockdeno install
    pyproject.tomluv.lockuv lock
    pyproject.tomlpoetry.lockpoetry lock --no-update
  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 and synced lock 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
--sign / -SGPG-sign the release commit and annotated tag
--skip-changelogBump version files without changelog generation
--forceAllow breaking changes in patch-only scheme
--stable [branch]Create a stable branch for patch-only releases (optional custom branch name)
--push [remote]Push commit and tags after release. Without a remote, pushes to origin
--minorUse minor bump (instead of major) when advancing main after --stable

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).

Early validation: when scheme = "calver", the calver_format is validated at config parse time. If the format is invalid, a warning is printed and the default format (YYYY.MM.PATCH) is used instead.

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.3.3 Monorepo Mode

When monorepo = true in .git-std.toml, git std bump performs per-package versioning in addition to the root version bump.

Package discovery:

  1. Explicit [[packages]] entries in config (highest priority).
  2. Auto-discovery from workspace manifests:
    • Cargo.toml [workspace] members
    • package.json workspaces
    • deno.json workspace
  3. Fallback: subdirectories containing version files.

Per-package bump algorithm:

  1. Collect all tags once.
  2. For each package: a. Find the latest tag matching tag_template for that package name. b. Walk commits between that tag and HEAD, filtered by the package path (git log --first-parent -- {path}). c. Parse conventional commits and determine bump level (same rules as root bump). d. Compute new version string.
  3. If scheme = "calver" for a package (via per-package scheme override or global), use date-based versioning: any commit touching the package path triggers a new version. Same date period increments the patch counter.

Dependency cascade:

After computing direct bumps, git-std resolves runtime dependencies from workspace manifests (Cargo, npm). If package A bumps and package B depends on A, B receives at least a patch bump. Dev-dependencies are excluded. The cascade iterates until stable (no new bumps).

The -p / --package flag skips cascade — only the named packages are bumped.

Apply phase:

  1. Write version files for each bumped package (respecting per-package [[packages.version_files]] overrides).
  2. Write per-package CHANGELOG.md in each package root (respecting per-package [packages.changelog] overrides).
  3. Write root CHANGELOG.md from all commits.
  4. Sync lock files once.
  5. Single commit: chore(release): v{root}, {pkg}@{ver}, ...
  6. Multiple tags: root v{version} + per-package tags from tag_template (default {name}@{version}).

Flags:

FlagDescription
--package <name>Filter bump to specific packages (repeatable)

All existing bump flags (--dry-run, --no-tag, --no-commit, --skip-changelog, --format json, etc.) work in monorepo mode.

Output (monorepo dry-run):

$ git std bump --dry-run

  core    0.2.0 → 0.3.0   minor — new feature
  cli     0.1.4 → 0.1.5   patch — dependency cascade (core)
  root    0.8.0 → 0.9.0   minor — new feature

  Tags:
    v0.9.0
    core@0.3.0
    cli@0.1.5

First release: when no tag and no version file exist for a package, the first semver release defaults to 0.1.0. Calver packages use the current date.

2.3.4 Lifecycle Hooks

git std bump supports four lifecycle hooks that run at defined points in the bump pipeline. Hook files live in .githooks/ with the .hooks extension, using the same format and sigils as git hooks (see §2.6).

Lifecycle positions:

                      ← pre-bump               (gate: validate, abort)
1. Detect version
2. Collect + parse commits
3. Compute new version
                      ← post-version {version} (build, generate artifacts)
4. Update version files
5. Sync lock files
6. Generate changelog
                      ← post-changelog         (lint, rewrite CHANGELOG.md)
7. Stage files
8. Create commit
9. Create tag
10. Push (if `--push`)
                      ← post-bump              (publish, notify — code is pushed)

Hook summary:

HookArgumentUse case
pre-bumpTests pass, clean tree, correct branch
post-version{version}Build with new version, stamp binaries
post-changelogLint/reformat CHANGELOG.md, rewrite URLs
post-bumpcargo publish, deploy, notify (after push)

Rules:

  • Missing hook file → silently skipped (not an error).
  • ! (required) commands: non-zero exit aborts the bump.
  • ? (advisory) commands: non-zero exit prints a warning, bump continues.
  • --dry-run skips all lifecycle hooks.
  • post-changelog is skipped when --skip-changelog is used.
  • post-version receives the computed version string as its first positional argument ($1).
  • Hook names do not collide with standard git hook names, so git std hook list / enable / disable work for bump hooks too.

Example:

# .githooks/pre-bump.hooks
!  just verify

# .githooks/post-version.hooks
!  cargo build --release
?  cp target/release/mybin dist/

# .githooks/post-changelog.hooks
?  dprint fmt CHANGELOG.md

# .githooks/post-bump.hooks
!  cargo publish
?  curl -X POST https://hooks.slack.com/...

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. Print to stdout (default) or write to file when -w is given.

Flags:

FlagDescription
--fullRegenerate the entire changelog from the first commit
--range <range>Generate changelog for a tag range (e.g. v1.0..v2.0)
-w, --write [path]Write to file (default: CHANGELOG.md)

Output goes to stdout by default. Pass -w / --write to write to CHANGELOG.md, or -w <path> to write to a custom file. 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 hook

Manage and execute git hooks defined in .githooks/*.hooks files.

2.5.1 git std hook 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 hook 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 hook list

Display all configured hooks and their commands:

$ git std hook 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 lint --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 lint --file {msg}

2.7 git std bootstrap

Post-clone environment setup. Detects convention files in the repository and configures the local environment.

2.7.1 Built-in checks

Convention fileActionCondition
.githooks/git config core.hooksPath .githooksdirectory exists
.gitattributesgit lfs install + git lfs pullcontains filter=lfs
.git-blame-ignore-revsgit config blame.ignoreRevsFile .git-blame-ignore-revsfile exists

If LFS rules are detected but git-lfs is not installed, prints an error with install URL and exits 1.

2.7.2 Custom commands (.githooks/bootstrap.hooks)

After built-in checks, runs .githooks/bootstrap.hooks through the existing hooks runner if the file exists. Uses the standard .hooks file format:

  • ! prefix = required (fail bootstrap on failure)
  • ? prefix = advisory (best-effort)
  • No prefix = default mode

Flags:

FlagDescription
--dry-runPrint what would be done without acting

Exit codes: 0 success (including nothing to do), 1 failure.

2.8 git std init

Single maintainer setup command. Consolidates hook setup and bootstrap scaffolding into one idempotent command that project maintainers run once and commit.

Steps performed:

  1. Creates .githooks/ directory.
  2. Sets core.hooksPath to .githooks.
  3. Writes .hooks templates (pre-commit, commit-msg, pre-push, and others).
  4. Prompts which hooks to enable; writes active shims and .off shims for the rest.
  5. Generates ./bootstrap script that installs git-std and delegates to git std bootstrap.
  6. Generates .githooks/bootstrap.hooks template.
  7. Creates .git-std.toml with a taplo #:schema directive and commented-out common fields (skipped silently if the file already exists).
  8. Appends a post-clone reminder to AGENTS.md and README.md (idempotent, HTML comment marker).
  9. Stages all created/modified files.

Two personas:

  • Maintainer: git std init — scaffold everything, commit the result.
  • Contributor: ./bootstrap — post-clone setup (calls git std bootstrap).

Flags:

FlagDescription
--forceOverwrite existing files

Exit codes: 0 success, 1 failure.

All steps are idempotent — running twice produces the same result.

2.8 git std version

Lightweight, scriptable version queries without the overhead of bump --dry-run.

git std version                  # 0.10.2
git std version --describe       # 0.10.2-dev.7+g3a2b1c.dirty
git std version --next           # 0.11.0
git std version --label          # minor
git std version --code           # 10299
git std version --format json    # all fields as JSON

Output is always to stdout. No v prefix — the command outputs a version value, not a git tag name.

Flags

FlagDescription
--describeCargo-style describe: -dev.N pre-release + +hash[.dirty] metadata
--nextNext version computed from conventional commits since the last tag
--labelBump label (major/minor/patch/none) accounting for pre-1.0
--codeInteger version code (see §2.8.1)
--format <fmt>text (default) or json

Multiple flags may be combined. With --format json all fields are always included regardless of which other flags are set.

--describe format

<version>[-dev.<N>][+g<hash>[.dirty]]

  • -dev.N — appended when HEAD is N commits ahead of the version tag.
  • +g<hash> — the first 7 characters of the HEAD SHA, prefixed with g. Present when HEAD is ahead of the tag.
  • .dirty — appended when the working tree has uncommitted changes.

Example: 0.10.2-dev.7+g3a2b1c.dirty

--label and pre-1.0 rule

For versions < 1.0.0, bump levels are downshifted following the Cargo pre-1.0 convention (same as git std bump):

  • Breaking change → minor
  • New feature → patch
  • Bug fix → patch

When no bump-worthy commits exist, the label is none.

--format json schema

{
  "version": "0.10.2",
  "describe": "0.10.2-dev.3+g1a2b3c4",
  "next": "0.11.0",
  "label": "minor",
  "code": 100299
}

Fields describe, next, label, and code are always present in JSON output (computed unconditionally). Fields may be null only if computation fails (e.g., no tags).

§2.8.1 Version code (--code)

A monotonically increasing integer encoding of the version, suitable for Android versionCode, iOS CFBundleVersion, or any platform that requires an integer build number.

Semver formula:

code = ((MAJOR × 1 000 + MINOR) × 100 + PATCH) × 100 + stage

Calver formula:

code = days_since_epoch × 10 000 + MICRO × 100 + stage

Where days_since_epoch is the number of days since 1970-01-01 for the first day of the calver period, and MICRO is the PATCH component.

Stage table (shared):

Pre-releaseStage
(unknown)1
dev9
dev.0dev.2810–38
alpha39
alpha.0alpha.1840–58
beta59
beta.0beta.1860–78
rc79
rc.0rc.1880–98
(stable)99

The stage range ensures every pre-release build is numerically less than the final stable release while remaining monotonically increasing within each pre-release track.

2.9 Update Check

git-std periodically checks whether a newer release is available and prints a one-line hint after command output.

Behaviour (gh-style background check):

  1. On every invocation, read the cache file (~/.config/git-std/update-check.json, respects $XDG_CONFIG_HOME).
  2. If the cache is missing or stale (>24 h), spawn a detached background process that queries the GitHub releases API and writes the cache. No hint is printed on this run.
  3. On the next invocation (cache fresh, newer version exists), print a hint after command output.
  4. The update command adapts to the detected install method (cargo install, install.sh, nix profile upgrade, or a link to the releases page).

Properties:

  • Never blocks CLI startup — background subprocess is fire-and-forget.
  • 24 h throttle — at most one network request per day.
  • Stale or missing cache = silence.
  • Opt-out: set GIT_STD_NO_UPDATE_CHECK=1.

2.10 Global Flags

FlagDescription
--help / -hPrint help
--version / -VPrint git-std version
--color <when>auto (default), always, never
--completions <shell>Generate shell completions to stdout

--completions <shell>

Generate shell completion scripts for bash, zsh, or fish. Works without a subcommand — usable regardless of install method.

git std --completions bash   # Bash completions
git std --completions zsh    # Zsh completions
git std --completions fish   # Fish completions

The generated scripts include wrappers that enable completion for both git-std (standalone) and git std (git subcommand) invocations.

Example — add to shell profile:

# Bash (~/.bashrc)
eval "$(git-std --completions bash)"

# Zsh (~/.zshrc)
eval "$(git-std --completions zsh)"

# Fish (~/.config/fish/config.fish)
git-std --completions fish | source

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
# "auto" discovers directory names from crates/*/, packages/*/, modules/*/
strict = true                                  # enforce types/scopes without --strict flag
monorepo = false                               # per-package versioning

# ── 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"
tag_template = "{name}@{version}"              # per-package tag format (monorepo)

# ── 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>'

# ── Packages (monorepo) ──────────────────────────────────
# [[packages]]
# name = "core"
# path = "crates/core"
# scheme = "patch"                             # optional override

Defaults when .git-std.toml is absent:

FieldDefault
schemesemver
typesfeat, fix, docs, style, refactor, perf, test, chore, ci, build, revert
scopesNone (no scope validation)
strictfalse
monorepofalse
versioning.tag_prefixv
versioning.prerelease_tagrc
versioning.calver_formatYYYY.MM.PATCH
versioning.tag_template{name}@{version}
changelog.hiddenchore, ci, build, style, test
changelog.sectionsSensible defaults for each type
version_files[] (empty — built-in files are always auto-detected)
packages[] (auto-discovered from workspace manifests when monorepo = true)

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 outputCHANGELOG.md at root; {path}/CHANGELOG.md per package
Release commitchore(release): <version> (includes all packages)
Package dependenciesResolved from workspace manifests (runtime only)

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 hook 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 lint "feat: add feature"

# All commits on your branch
git std lint --range main..HEAD

# Strict mode (enforce known types and scopes)
git std lint --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 hook list

Run manually (debugging):

git std hook run pre-commit

Regenerate shims after editing .hooks files:

git std hook 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 lint --range $CI_MERGE_REQUEST_DIFF_BASE_SHA..HEAD
# GitHub Actions
- name: Validate commits
  run: git std lint --range ${{ github.event.pull_request.base.sha }}..${{ github.sha }}

JSON output for scripting:

git std lint --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. Implemented. Three-way: not set (default, no validation), scopes = "auto" (discover from crates/*/, packages/*/, modules/*/ directory names), 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.