hooks: branch guard, 300-line cap, task ref, auto PR (V-445)
Four git hooks, tracked in .githooks and wired with core.hooksPath so a fresh clone gets them with one config line. pre-commit refuses master and refuses more than 300 changed lines in non-markdown files. Markdown is exempt because docs land as one batch. commit-msg requires (V-<id>), not (#<id>), because Gitea autolinks # to its own issues and that is the wrong tracker. post-checkout records the parent branch, since git does not track where a branch was cut from and the PR needs the base. pre-push opens the PR with tea and never blocks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+30
@@ -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
|
||||
Executable
+34
@@ -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: <prev-head> <new-head> <branch-flag>. 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
|
||||
Executable
+30
@@ -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 <vikunja-id> <slug> # branch, and record the parent" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Added + deleted lines across staged files that are not markdown.
|
||||
# numstat prints "-\t-\t<path>" 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 <path> to unstage, or --no-verify if this genuinely cannot split." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Executable
+33
@@ -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
|
||||
Executable
+29
@@ -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 <vikunja-id> <slug>" >&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)."
|
||||
Reference in New Issue
Block a user