Skip to content
Merged
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
37 changes: 27 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ We keep the code simple and consistent:
`scripts/rules.sh` holds the conventions a person can break in a second: a
comment where the code should speak for itself, `unsafe` outside the `-sys`
crates, the core printing to a terminal or reaching for a platform, Spanish in
an identifier, voseo or peninsular words in what a person reads, two crates
naming an example the same, and a file grown past what anybody reads through.
`scripts/commits.sh` holds the shape and the length of a subject. Both answer
an identifier, two crates naming an example the same, and a file grown past
what anybody reads through. `scripts/commits.sh` holds the commit convention
below. Both answer
the same whether you run them or CI does, and they say every rule that broke
rather than stopping at the first. Run either whenever you like, and if you want
them run for you:
Expand All @@ -134,9 +134,9 @@ git config core.hooksPath hooks

That gives you three. `pre-commit` runs the conventions, `cargo fmt --all
--check` and biome — a couple of seconds, and between them they are most of what
turns CI red. `commit-msg` weighs the subject while the fix is still an
turns CI red. `commit-msg` weighs the message while the fix is still an
`--amend` rather than a rebase. `pre-push` runs the conventions again and the
subjects of everything you are about to send, and when what you are sending is a
messages of everything you are about to send, and when what you are sending is a
tag it asks `scripts/news.sh` whether `app/src/news.json` says what changed in
that version — the screen that tells a person what is new is the only place the
app says it, and a tag is the last moment to notice it is empty. A candidate is
Expand All @@ -145,6 +145,25 @@ is above `3.0.0-rc1`. Nothing slower goes in any of
them: the suite, the build and the markdown lint are minutes, and they belong to
CI.

### Commit messages

One line, in English, and nothing else:

```text
type(scope): the concrete change
```

- **Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`,
`build`, `ci`, `chore`, `revert`. The scope is optional.
- **At most 120 characters.**
- **No body and no trailers** — no `Co-authored-by`, no `Signed-off-by`. The
CLA below is signed once on the pull request, not on each commit.
- **No links**, except `#123` for an issue or pull request of this repository.
- Say what changed, not how it was found.

A pull request's title follows the same rule, because the squash keeps it as
the subject on `main`.

**UI/UX:**

- Native look and feel on each platform (Windows, macOS)
Expand Down Expand Up @@ -179,11 +198,9 @@ I have read the CLA Document and I hereby sign the CLA

That is it — every later Pull Request from the same account is covered.

**Please leave tool co-authorship out of your commits.** Assistants are welcome
here — this project is built with them — but the credit line is for people. If
your editor adds a trailer naming one, drop it before you push. It changes
nothing about what you are allowed to submit; section 4 of the CLA already puts
the responsibility for generated code on you, whichever tool helped write it.
**Commits carry no trailers.** If your editor adds one, drop it before you
push. Section 4 of the CLA already puts the responsibility for what you submit
on you, whatever helped you write it.

**In return, the project commits that:**

Expand Down
5 changes: 1 addition & 4 deletions hooks/commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,4 @@ set -uo pipefail

cd "$(git rev-parse --show-toplevel)" || exit 0

said=$(sed '/^# -* >8 -*$/,$d' "$1" | git stripspace | sed -n '1,/^$/p' | paste -sd' ' -)
[ -n "$said" ] || exit 0

exec bash scripts/commits.sh --subject "$said"
exec bash scripts/commits.sh --message "$1"
8 changes: 2 additions & 6 deletions hooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,10 @@ while read -r ref mine _ theirs; do
esac

if [ "${theirs:-$nothing}" != "$nothing" ]; then
range="$theirs..$mine"
elif base=$(git merge-base "$mine" origin/main 2> /dev/null) && [ -n "$base" ]; then
range="$base..$mine"
bash scripts/commits.sh "$theirs..$mine" || status=1
else
echo "! origin/main is not here, so the subjects were not looked at"
continue
bash scripts/commits.sh "$mine" --not --remotes || status=1
fi
bash scripts/commits.sh "$range" || status=1
done

exit $status
127 changes: 90 additions & 37 deletions scripts/commits.sh
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#!/usr/bin/env bash
set -uo pipefail

shape='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9._-]+\))?!?: .+'
most=90
shape='^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9._-]+\))?: .+'
elsewhere='https?://[^[:space:]/]+\.|(^|[[:space:](<])www\.|(^|[[:space:](<])([[:alnum:]-]+\.)+[[:alpha:]]{2,}/|[[:alnum:]._-]+@[[:alnum:].-]+:|[[:alnum:]_.-]+/[[:alnum:]_.-]+#[0-9]+'
most=120
status=0

amiss() {
Expand All @@ -14,83 +15,135 @@ amiss() {
status=1
}

# commit-msg runs on git merge too, where a refusal leaves it half done
git_wrote() {
case $1 in
"Merge branch '"* | "Merge branches "* | "Merge pull request #"* | \
"Merge remote-tracking branch '"* | "Merge commit '"* | "Merge tag '"* | \
"Merge http://"* | "Merge https://"* | "Merge git://"* | "Merge ssh://"* | \
"Squashed commit of the following:"* | 'Revert "'* | 'Reapply "'* | \
"fixup! "* | "squash! "* | "amend! "*)
return 0
;;
esac
return 1
}

trimmed() {
printf '%s' "$1" | LC_ALL=C sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
}

# strict is for a title, which reaches main: only what git writes itself keeps its shape there
weighed() {
local who=$1 said=$2 strict=${3:-}
said=$(printf '%s' "$said" | LC_ALL=C sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
if [ -z "$strict" ]; then
# every shape git writes itself, and commit-msg runs on git merge, where a refusal leaves it half done
case $said in
"Merge branch '"* | "Merge branches "* | "Merge pull request #"* | \
"Merge remote-tracking branch '"* | "Merge commit '"* | "Merge tag '"* | \
"Merge http://"* | "Merge https://"* | "Merge git://"* | "Merge ssh://"* | \
"Squashed commit of the following:"* | 'Revert "'* | 'Reapply "'* | \
"fixup! "* | "squash! "* | "amend! "*)
return
;;
esac
local who=$1 said strict=${3:-}
said=$(trimmed "$2")
if [ -z "$strict" ] && git_wrote "$said"; then
return
fi
local broke=""
case $said in
'Revert "'* | 'Reapply "'*) ;;
*)
if ! printf '%s' "$said" | grep -qE "$shape"; then
broke=1
amiss "$who does not follow the convention"
printf ' %s\n' "$said"
return
fi
;;
esac
if printf '%s' "$said" | grep -qiE "$elsewhere"; then
broke=1
amiss "$who links outside this repository; only #123 of this one"
fi
# bytes minus UTF-8 continuation bytes: ${#said} counts bytes when a client spawns git with no locale
local long
long=$(printf '%s' "$said" | LC_ALL=C tr -d '\200-\277' | wc -c | tr -d ' ')
if [ "$long" -gt "$most" ]; then
broke=1
amiss "$who is $long characters, and the subject goes under $most"
printf ' %s\n' "$said"
fi
[ -z "$broke" ] || printf ' %s\n' "$said"
}

bodiless() {
local who=$1 subject body
subject=$(trimmed "$2")
body=$(trimmed "$3")
if [ -n "$body" ] && ! git_wrote "$subject"; then
amiss "$who carries more than its subject line: a body, a trailer or a wrapped subject"
printf ' %s\n' "$subject"
fi
}

how_it_reads() {
if [ "$status" -eq 1 ]; then
printf '\n'
printf 'Expected: type(optional scope): description\n'
printf 'Expected: type(optional scope): concrete change, one line, in English\n'
printf 'Types: feat fix docs style refactor perf test build ci chore revert\n'
printf 'Example: fix(panel): the menu yields to the wheel\n'
fi
exit $status
}

if [ "${1:-}" = "--subject" ] || [ "${1:-}" = "--title" ]; then
[ -n "${2:-}" ] || {
echo "usage: commits.sh --subject|--title <text>"
exit 2
}
weighed "the subject" "$2" "$([ "$1" = "--title" ] && echo strict)"
[ "$status" -eq 0 ] && [ -z "${GITHUB_ACTIONS:-}" ] && printf 'ok the subject is well formed\n'
how_it_reads
fi
case ${1:-} in
--title)
[ -n "${2:-}" ] || {
echo "usage: commits.sh --title <text>"
exit 2
}
weighed "the title" "$2" strict
[ "$status" -eq 0 ] && [ -z "${GITHUB_ACTIONS:-}" ] && printf 'ok the title is well formed\n'
how_it_reads
;;
--message)
[ -f "${2:-}" ] || {
echo "usage: commits.sh --message <file>"
exit 2
}
# git strips its own comment lines after this hook runs, and only when an editor wrote them
mark=$(git config core.commentChar 2> /dev/null || echo '#')
case $mark in auto | "") mark='#' ;; esac
said=$(awk -v m="$mark" '
index($0, m " ") == 1 && $0 ~ / -+ >8 -+$/ { exit }
$0 == m || index($0, m " ") == 1 || index($0, m "\t") == 1 { next }
{ print }
' "$2" | git stripspace)
[ -n "$said" ] || exit 0
subject=$(printf '%s\n' "$said" | head -1)
weighed "the subject" "$subject"
bodiless "the message" "$subject" "$(printf '%s\n' "$said" | tail -n +2)"
[ "$status" -eq 0 ] && [ -z "${GITHUB_ACTIONS:-}" ] && printf 'ok the message is well formed\n'
how_it_reads
;;
esac

range=${1:-}
[ -n "$range" ] || {
echo "usage: commits.sh <range> | --subject <text> | --title <text>"
[ $# -gt 0 ] || {
echo "usage: commits.sh <revisions…> | --title <text> | --message <file>"
exit 2
}

# git prints hints on stderr while exiting 0, and folding them in would weigh them as subjects
# git prints hints on stderr while exiting 0, and folding them in would weigh them as messages
trouble=$(mktemp)
if ! listed=$(git rev-list --no-merges "$range" 2> "$trouble"); then
if ! listed=$(git log --no-merges --format='%h%x1f%an%x1f%B%x1e' "$@" 2> "$trouble"); then
cat "$trouble" >&2
rm -f "$trouble"
amiss "the commits between $range could not be listed, so no subject was looked at"
amiss "the commits in $* could not be listed, so no message was looked at"
exit 1
fi
rm -f "$trouble"

seen=0
while read -r sha; do
while IFS=$'\x1f' read -r -d $'\x1e' sha name message; do
sha=$(trimmed "$sha")
[ -n "$sha" ] || continue
seen=$((seen + 1))
weighed "${sha:0:8}" "$(git log -1 --format=%s "$sha")"
subject=$(printf '%s\n' "$message" | head -1)
weighed "$sha" "$subject"
# a bot writes its own body with no way to leave it out; its squash title is what reaches main
case $name in
*"[bot]") ;;
*) bodiless "$sha" "$subject" "$(printf '%s\n' "$message" | tail -n +2)" ;;
esac
done <<< "$listed"

[ "$status" -eq 0 ] && [ -z "${GITHUB_ACTIONS:-}" ] && printf 'ok %s commit subject(s) well formed\n' "$seen"
[ "$status" -eq 0 ] && [ -z "${GITHUB_ACTIONS:-}" ] && printf 'ok %s commit message(s) well formed\n' "$seen"
how_it_reads
19 changes: 0 additions & 19 deletions scripts/rules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,24 +62,6 @@ written_in_english() {
&& went_well "no Spanish identifiers"
}

neutral_spanish() {
local voseo='\b(v[o]s|ten[é]s|quer[é]s|pod[é]s|and[á]|mir[á]|hac[é]|ch[e])\b'
local peninsular='\b(fichero|ficheros|ordenador|pulsa|pulsar|pulsando)\b'
# a path that is not there is an error to grep, so only what exists is handed over
local -a where=(crates README.md)
[ -d docs ] && where+=(docs)
grep -rniE "$voseo" "${where[@]}" --include='*.rs' --include='*.md'
found_nothing \
"neutral Spanish, no voseo" $? \
"the Spanish could not be looked through for voseo" \
&& went_well "no voseo anywhere"
grep -rniE "$peninsular" "${where[@]}" --include='*.rs' --include='*.md'
found_nothing \
"neutral Spanish: archivo, computador, presiona" $? \
"the Spanish could not be looked through for peninsular words" \
&& went_well "no peninsular Spanish in what a person reads"
}

no_comments_in_the_code() {
grep -rnE '[/]{2}' crates app/src-tauri/src --include='*.rs' \
| grep -vE '[a-z]+:[/][/]' | grep -vE '"[^"]*[/]{2}'
Expand Down Expand Up @@ -135,7 +117,6 @@ unsafe_only_in_the_sys_crates
nothing_the_core_prints
nothing_the_core_depends_on
written_in_english
neutral_spanish
no_comments_in_the_code
every_example_has_its_own_name
what_python_measures "the tests live beside the file, not inside it" oversized.py --inline
Expand Down
Loading