diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 0000000..b69caf1 --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,30 @@ +#!/bin/sh +# Every commit names the Vikunja task it belongs to. +# +# router: narrow the single-token rule (V-359) +# +# V- and not #, because Gitea autolinks #359 to a Gitea issue, which is a +# different tracker and a wrong link. +# +# Exempt: merges, reverts, fixup/squash, and the initial commit. + +msg_file=$1 +subject=$(sed -n '1p' "$msg_file") + +case "$subject" in + Merge\ *|Revert\ *|fixup!\ *|squash!\ *|amend!\ *) exit 0 ;; +esac + +if [ -f "$(git rev-parse --git-dir)/MERGE_HEAD" ]; then + exit 0 +fi + +if printf '%s' "$subject" | grep -qE '\(V-[0-9]+\)$'; then + exit 0 +fi + +echo "commit-msg: subject must end with a Vikunja task ref." >&2 +echo " got: $subject" >&2 +echo " want: router: narrow the single-token rule (V-359)" >&2 +echo " No task yet? Create one. Work without a task is work nobody can resume." >&2 +exit 1 diff --git a/.githooks/post-checkout b/.githooks/post-checkout new file mode 100755 index 0000000..abb16be --- /dev/null +++ b/.githooks/post-checkout @@ -0,0 +1,34 @@ +#!/bin/sh +# Git does not remember which branch a branch was cut from, and the PR needs it. +# Record it once, at creation, best effort. +# +# Args: . branch-flag is 1 for a branch +# switch, 0 for a file checkout. + +prev=$1 +flag=$3 + +[ "$flag" = "1" ] || exit 0 + +branch=$(git symbolic-ref --short HEAD 2>/dev/null) || exit 0 +[ -n "$branch" ] || exit 0 + +dir="$(git rev-parse --git-dir)/maven-parent" +key=$(printf '%s' "$branch" | tr '/' '_') + +# Already recorded, or we are on a long-lived branch. Leave it alone. +[ -f "$dir/$key" ] && exit 0 +case "$branch" in master|main) exit 0 ;; esac + +# name-rev resolves the sha we came from back to a branch name. It picks the +# closest ref, which is right often enough to beat guessing master. +parent=$(git name-rev --name-only --refs='refs/heads/*' "$prev" 2>/dev/null | + sed 's/[~^].*//') + +case "$parent" in + ""|undefined|"$branch") parent=master ;; +esac + +mkdir -p "$dir" +printf '%s\n' "$parent" > "$dir/$key" +exit 0 diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..7caf64f --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,30 @@ +#!/bin/sh +# Two guards, both bypassable with --no-verify when you mean it. +# 1. master is not a working branch. +# 2. a code commit stays under 300 changed lines. +# Markdown is exempt from the size cap on purpose: docs land as one batch. + +branch=$(git symbolic-ref --short HEAD 2>/dev/null) + +case "$branch" in + master|main) + echo "pre-commit: refusing to commit on $branch." >&2 + echo " scripts/task-branch.sh # branch, and record the parent" >&2 + exit 1 + ;; +esac + +# Added + deleted lines across staged files that are not markdown. +# numstat prints "-\t-\t" for binaries; those count 0 and that is fine, +# a binary blob is not the kind of diff this cap exists to stop. +loc=$(git diff --cached --numstat -- . ':(exclude)*.md' | + awk '$1 ~ /^[0-9]+$/ { a += $1 } $2 ~ /^[0-9]+$/ { d += $2 } END { print a + d + 0 }') + +if [ "$loc" -gt 300 ]; then + echo "pre-commit: $loc changed lines in non-markdown files, cap is 300." >&2 + echo " Split it. Each commit should be one reviewable idea." >&2 + echo " git reset to unstage, or --no-verify if this genuinely cannot split." >&2 + exit 1 +fi + +exit 0 diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..0215439 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,33 @@ +#!/bin/sh +# Open the PR on first push of a task branch, against the branch it was cut +# from. A commit is too early for this. A push means the work is shareable. +# +# Never fatal: a missing tea login or an unreachable Gitea must not block a +# push. It prints the command instead. + +branch=$(git symbolic-ref --short HEAD 2>/dev/null) || exit 0 +case "$branch" in ""|master|main) exit 0 ;; esac + +command -v tea >/dev/null 2>&1 || exit 0 + +dir="$(git rev-parse --git-dir)/maven-parent" +key=$(printf '%s' "$branch" | tr '/' '_') +parent=$(cat "$dir/$key" 2>/dev/null) +[ -n "$parent" ] || parent=master + +if tea pr list --output simple 2>/dev/null | grep -q "[[:space:]]$branch\$"; then + exit 0 +fi + +title=$(git log -1 --format=%s) +body=$(git log "$parent..$branch" --format='- %s' 2>/dev/null) + +if tea pr create --head "$branch" --base "$parent" \ + --title "$title" --description "$body" >/dev/null 2>&1; then + echo "pre-push: opened PR $branch -> $parent" +else + echo "pre-push: could not open the PR. Run it yourself:" >&2 + echo " tea pr create --head $branch --base $parent --title \"$title\"" >&2 +fi + +exit 0 diff --git a/scripts/task-branch.sh b/scripts/task-branch.sh new file mode 100755 index 0000000..c7fa648 --- /dev/null +++ b/scripts/task-branch.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# Cut a task branch and record the branch it came from, so pre-push can open +# the PR against the right base. +# +# scripts/task-branch.sh 359 narrow-single-token +# -> task/359-narrow-single-token, parent recorded as the current branch + +set -eu + +if [ $# -lt 2 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +id=$1 +shift +slug=$(printf '%s' "$*" | tr '[:upper:] ' '[:lower:]-' | tr -cd 'a-z0-9-') + +parent=$(git symbolic-ref --short HEAD) +branch="task/$id-$slug" + +git checkout -b "$branch" + +dir="$(git rev-parse --git-dir)/maven-parent" +mkdir -p "$dir" +printf '%s\n' "$parent" > "$dir/$(printf '%s' "$branch" | tr '/' '_')" + +echo "$branch, cut from $parent" +echo "Commits on it must end with (V-$id)."