Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
40 changes: 27 additions & 13 deletions .rhiza/completions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment on lines +15 to +28

### Bash

#### Method 1: Source in your shell config
Expand Down Expand Up @@ -102,7 +117,7 @@ make <TAB>
make te<TAB> # Expands to: make test

# Complete variables
make BUMP=<TAB> # Shows: patch, minor, major
make ENV=<TAB> # Shows: dev, staging, prod

# Works with any target
make doc<TAB> # Shows: docs, docker-build, docker-run, etc.
Expand All @@ -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 |
Expand All @@ -141,10 +155,10 @@ Example usage:
make DRY_<TAB> # Expands to: make DRY_RUN=1

# Tab-complete variable values
make BUMP=<TAB> # Shows: patch minor major
make ENV=<TAB> # Shows: dev staging prod

# Combine with targets
make bump BUMP=<TAB>
make deploy ENV=<TAB>
```

## Troubleshooting
Expand Down Expand Up @@ -244,20 +258,20 @@ m te<TAB> # 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*
44 changes: 36 additions & 8 deletions .rhiza/completions/rhiza-completion.bash
Original file line number Diff line number Diff line change
Expand Up @@ -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]}"
Expand All @@ -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
Comment on lines +48 to +56

# 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
Expand Down
56 changes: 42 additions & 14 deletions .rhiza/completions/rhiza-completion.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -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/ {
Expand All @@ -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
Comment on lines +75 to +90

# 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'
Expand Down
91 changes: 91 additions & 0 deletions cliff.toml
Original file line number Diff line number Diff line change
@@ -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 = """
<!-- generated by git-cliff -->
"""
# 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 <dependency>` — 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 = "<!-- 0 -->New Features" },
{ message = "^fix", group = "<!-- 1 -->Bug Fixes" },
{ message = "^docs?", group = "<!-- 2 -->Documentation" },
{ message = "^perf", group = "<!-- 3 -->Performance" },
{ message = "^(build|chore)\\(deps[^)]*\\):", group = "<!-- 4 -->Dependencies" },
{ message = "^refactor", group = "<!-- 5 -->Maintenance" },
{ message = "^style", group = "<!-- 5 -->Maintenance" },
{ message = "^chore", group = "<!-- 5 -->Maintenance" },
{ message = "^build", group = "<!-- 5 -->Maintenance" },
{ message = "^ci", group = "<!-- 5 -->Maintenance" },
{ message = "^test", group = "<!-- 5 -->Maintenance" },
{ message = "^revert", group = "<!-- 6 -->Reverts" },
{ message = ".*", group = "<!-- 7 -->Other Changes" },
]
1 change: 0 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
--8<-- "README.md"