diff --git a/.gitignore b/.gitignore index 3c08606..1c9d3f0 100644 --- a/.gitignore +++ b/.gitignore @@ -133,3 +133,9 @@ local.mk .bandit-baseline.json + +# Rust (rust-core bundle). Kept in core rather than the language layer because +# .gitignore has one owner and git opens it with O_NOFOLLOW, so it cannot be a +# per-layer file. These entries are inert in a Python repo. +target/ +**/*.rs.bk diff --git a/.rhiza/completions/README.md b/.rhiza/completions/README.md index b37cb15..ff62b4d 100644 --- a/.rhiza/completions/README.md +++ b/.rhiza/completions/README.md @@ -6,12 +6,27 @@ This directory contains shell completion scripts for Bash and Zsh that provide t - ✅ Tab-complete all available make targets - ✅ Show target descriptions in Zsh -- ✅ Complete common make variables (DRY_RUN, BUMP, ENV, etc.) +- ✅ Complete common make variables (DRY_RUN, ENV, etc.) - ✅ Works with any Rhiza-based project - ✅ Auto-discovers targets from Makefile and included .mk files ## Installation +### Quick install (recommended) + +From the project root: + +```bash +make install-completions # install for both bash and zsh +make install-completions SHELL_KIND=zsh # or just one: bash | zsh | both +``` + +This copies the appropriate script into your user completion directory +(`${XDG_DATA_HOME:-~/.local/share}/bash-completion/completions/make` for bash, +`${XDG_DATA_HOME:-~/.local/share}/zsh/site-functions/_make` for zsh) and prints +any follow-up step. Start a new shell afterwards. The manual methods below remain +available if you prefer to wire it up yourself. + ### Bash #### Method 1: Source in your shell config @@ -102,7 +117,7 @@ make make te # Expands to: make test # Complete variables -make BUMP= # Shows: patch, minor, major +make ENV= # Shows: dev, staging, prod # Works with any target make doc # Shows: docs, docker-build, docker-run, etc. @@ -129,7 +144,6 @@ The completion scripts understand these common variables: | Variable | Values | Description | |----------|--------|-------------| | `DRY_RUN` | `1` | Preview mode without making changes | -| `BUMP` | `patch`, `minor`, `major` | Version bump type | | `ENV` | `dev`, `staging`, `prod` | Target environment | | `COVERAGE_FAIL_UNDER` | (number) | Minimum coverage threshold | | `PYTHON_VERSION` | (version) | Override Python version | @@ -141,10 +155,10 @@ Example usage: make DRY_ # Expands to: make DRY_RUN=1 # Tab-complete variable values -make BUMP= # Shows: patch minor major +make ENV= # Shows: dev staging prod # Combine with targets -make bump BUMP= +make deploy ENV= ``` ## Troubleshooting @@ -244,20 +258,20 @@ m te # Expands to: m test 1. **Target Discovery**: Parses `make -qp` output to find all targets 2. **Description Extraction**: Looks for `##` comments after target names 3. **Variable Detection**: Includes common Makefile variables -4. **Dynamic Completion**: Regenerates list each time you tab +4. **Cached Completion**: The target list is cached per directory and refreshed automatically ### Performance -- Completions are generated on-demand (when you press Tab) -- For large Makefiles (100+ targets), there may be a small delay -- Results are not cached to ensure targets are always current +- The target list is cached under `${XDG_CACHE_HOME:-~/.cache}/rhiza/`, keyed per directory +- The cache refreshes automatically whenever the `Makefile`, `local.mk`, + `.rhiza/rhiza.mk`, or any `.rhiza/make.d/*.mk` file changes +- Only the first Tab press after a makefile change pays the full `make -qp` parsing cost +- To force a refresh manually, delete the cache: `rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}/rhiza"` +- If the cache directory cannot be created (e.g. read-only home), completion + falls back to direct parsing on every Tab press ## See Also - [Tools Reference](../../docs/reference/TOOLS_REFERENCE.md) - Complete command reference - [Quick Reference](../../docs/guides/QUICK_REFERENCE.md) - Quick command reference - [Extending Rhiza](../../docs/guides/EXTENDING_RHIZA.md) - How to add custom targets - ---- - -*Last updated: 2026-02-15* diff --git a/.rhiza/completions/rhiza-completion.bash b/.rhiza/completions/rhiza-completion.bash index 0f860da..eca9e8a 100644 --- a/.rhiza/completions/rhiza-completion.bash +++ b/.rhiza/completions/rhiza-completion.bash @@ -9,8 +9,19 @@ # sudo cp .rhiza/completions/rhiza-completion.bash /etc/bash_completion.d/rhiza # +# Return 0 (stale) when the cache file is missing or any makefile source +# changed since it was written. +_rhiza_make_cache_stale() { + local cache_file="$1" src + [[ -f "$cache_file" ]] || return 0 + for src in Makefile local.mk .rhiza/rhiza.mk .rhiza/make.d/*.mk; do + [[ -f "$src" && "$src" -nt "$cache_file" ]] && return 0 + done + return 1 +} + _rhiza_make_completion() { - local cur prev opts + local cur prev opts cache_dir cache_file COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" @@ -20,15 +31,32 @@ _rhiza_make_completion() { return 0 fi - # Extract make targets from Makefile and all included .mk files - # Looks for lines like: target: ## description - opts=$(make -qp 2>/dev/null | \ - awk -F':' '/^[a-zA-Z0-9][^$#\/\t=]*:([^=]|$)/ {split($1,A,/ /);for(i in A)print A[i]}' | \ - grep -v '^Makefile$' | \ - sort -u) + # Target extraction parses the full make database (make -qp), which is + # slow on large Makefiles - cache the result per directory and refresh + # only when a makefile source changes. + cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/rhiza" + cache_file="$cache_dir/targets-$(pwd | cksum | cut -d' ' -f1)" + + if _rhiza_make_cache_stale "$cache_file" && mkdir -p "$cache_dir" 2>/dev/null; then + # Extract make targets from Makefile and all included .mk files + make -qp 2>/dev/null | \ + awk -F':' '/^[a-zA-Z0-9][^$#\/\t=]*:([^=]|$)/ {split($1,A,/ /);for(i in A)print A[i]}' | \ + grep -v '^Makefile$' | \ + sort -u > "$cache_file" + fi + + if [[ -r "$cache_file" ]]; then + opts=$(cat "$cache_file") + else + # Cache unavailable (e.g. unwritable HOME): fall back to direct parsing + opts=$(make -qp 2>/dev/null | \ + awk -F':' '/^[a-zA-Z0-9][^$#\/\t=]*:([^=]|$)/ {split($1,A,/ /);for(i in A)print A[i]}' | \ + grep -v '^Makefile$' | \ + sort -u) + fi # Add common make variables that can be overridden - local vars="DRY_RUN=1 BUMP=patch BUMP=minor BUMP=major ENV=dev ENV=staging ENV=prod" + local vars="DRY_RUN=1 ENV=dev ENV=staging ENV=prod" opts="$opts $vars" # Generate completions diff --git a/.rhiza/completions/rhiza-completion.zsh b/.rhiza/completions/rhiza-completion.zsh index 03931c9..f1c3891 100644 --- a/.rhiza/completions/rhiza-completion.zsh +++ b/.rhiza/completions/rhiza-completion.zsh @@ -19,17 +19,34 @@ # sudo cp .rhiza/completions/rhiza-completion.zsh /usr/local/share/zsh/site-functions/_make # +# Return 0 (stale) when the cache file is missing or any makefile source +# changed since it was written. +_rhiza_make_cache_stale() { + local cache_file="$1" src + [[ -f "$cache_file" ]] || return 0 + for src in Makefile local.mk .rhiza/rhiza.mk .rhiza/make.d/*.mk(N); do + [[ -f "$src" && "$src" -nt "$cache_file" ]] && return 0 + done + return 1 +} + _rhiza_make() { local -a targets variables - + local cache_dir cache_file + # Check if we're in a directory with a Makefile if [[ ! -f "Makefile" ]]; then return 0 fi - # Extract make targets with descriptions - # Format: target:description - targets=(${(f)"$( + # Target extraction parses the full make database (make -qp) twice, which + # is slow on large Makefiles - cache both lists per directory and refresh + # only when a makefile source changes. + cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/rhiza" + cache_file="$cache_dir/targets-$(pwd | cksum | cut -d' ' -f1)" + + if _rhiza_make_cache_stale "$cache_file.desc" && mkdir -p "$cache_dir" 2>/dev/null; then + # Extract make targets with descriptions (format: target:description) make -qp 2>/dev/null | \ awk -F':' ' /^# Files/,/^# Finished Make data base/ { @@ -43,27 +60,38 @@ _rhiza_make() { } ' | \ grep -v '^Makefile:' | \ - sort -u - )"}) + sort -u > "$cache_file.desc" - # Also get targets without descriptions - local -a plain_targets - plain_targets=(${(f)"$( + # Also get targets without descriptions make -qp 2>/dev/null | \ awk -F':' '/^[a-zA-Z0-9_-]+:([^=]|$)/ { split($1,A,/ /) for(i in A) print A[i] }' | \ grep -v '^Makefile$' | \ - sort -u - )"}) + sort -u > "$cache_file.plain" + fi + + local -a plain_targets + if [[ -r "$cache_file.desc" ]]; then + targets=(${(f)"$(cat "$cache_file.desc")"}) + plain_targets=(${(f)"$(cat "$cache_file.plain" 2>/dev/null)"}) + else + # Cache unavailable (e.g. unwritable HOME): fall back to direct parsing + plain_targets=(${(f)"$( + make -qp 2>/dev/null | \ + awk -F':' '/^[a-zA-Z0-9_-]+:([^=]|$)/ { + split($1,A,/ /) + for(i in A) print A[i] + }' | \ + grep -v '^Makefile$' | \ + sort -u + )"}) + fi # Common make variables variables=( 'DRY_RUN=1:preview mode without making changes' - 'BUMP=patch:bump patch version' - 'BUMP=minor:bump minor version' - 'BUMP=major:bump major version' 'ENV=dev:development environment' 'ENV=staging:staging environment' 'ENV=prod:production environment' diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..6b1e880 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,91 @@ +# cliff.toml — git-cliff configuration for CHANGELOG generation. +# +# Synced from the rhiza `core` bundle. Drives `make changelog`, which runs +# `uvx git-cliff --output CHANGELOG.md`. +# +# This template is intentionally forge-agnostic: it does not hard-code an +# owner/repo. Pull-request and issue references like `(#123)` are left intact +# in the generated entries, and both GitHub and GitLab auto-link bare `#123` +# references when rendering Markdown inside a repository. Downstream projects +# that want richer links (full URLs, contributor handles) can enable +# git-cliff's remote integration — see https://git-cliff.org/docs/integration. +# +# Reference: https://git-cliff.org/docs/configuration + +[changelog] +# A markdown header rendered once at the top of the changelog. +header = """ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com), +and entries are generated from [Conventional Commits](https://www.conventionalcommits.org). + +""" +# The body is a Tera template rendered once per release. +# https://keats.github.io/tera/docs/#introduction +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [Unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %}\ + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | split(pat="\n") | first | trim | upper_first }} + {% endfor %}\ +{% endfor %}\n +""" +footer = """ + +""" +# Trim leading/trailing whitespace from the rendered body. +trim = true + +[git] +# Parse commits according to the Conventional Commits spec. +conventional_commits = true +# Keep non-conventional commits too, grouped under "Other Changes". +filter_unconventional = false +# Do not split a commit into multiple entries on newlines. +split_commits = false +# Do not drop commits that fail to match a parser below. +filter_commits = false +# Skip merge commits. +filter_merge_commits = true +# Match release tags (v1.2.3, v0.5.1, ...). +tag_pattern = "v[0-9].*" +# Order commits within a section oldest-first. +sort_commits = "oldest" +# Group commits into changelog sections. The leading HTML comment controls the +# section ordering and is stripped from the rendered heading via `striptags`. +commit_parsers = [ + # Drop automated noise commits that don't provide user-facing signal. + { message = ".*\\[skip ci\\].*", skip = true }, + # Only the release flow's own commits. A bare `bump` alternative here also ate every + # `chore(deps): bump ` — the rhiza-hooks v1.2.0 bump (#1487) vanished from + # v1.3.2's notes that way, and had been vanishing for a while unnoticed: a Dependabot + # subject escapes by the accident of its doubled `(deps)(deps)` scope, and #1482's + # identical bump survived only because it was typed `fix(deps):`. So this names + # `release` and the older `bump version` form explicitly rather than `bump` at large. + { message = "^chore(\\([^)]+\\))?:\\s*release\\b", skip = true }, + { message = "^chore(\\([^)]+\\))?:\\s*bump version\\b", skip = true }, + { message = "^chore:\\s*update changelog\\.md\\b", skip = true }, + { message = "^feat", group = "New Features" }, + { message = "^fix", group = "Bug Fixes" }, + { message = "^docs?", group = "Documentation" }, + { message = "^perf", group = "Performance" }, + { message = "^(build|chore)\\(deps[^)]*\\):", group = "Dependencies" }, + { message = "^refactor", group = "Maintenance" }, + { message = "^style", group = "Maintenance" }, + { message = "^chore", group = "Maintenance" }, + { message = "^build", group = "Maintenance" }, + { message = "^ci", group = "Maintenance" }, + { message = "^test", group = "Maintenance" }, + { message = "^revert", group = "Reverts" }, + { message = ".*", group = "Other Changes" }, +] diff --git a/docs/index.md b/docs/index.md index 0e0b6b4..612c7a5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,2 +1 @@ --8<-- "README.md" -