Nobody being able to answer "what changed last week" without reading 200 commit messages.
Add "Automatic build log" from ntent to this repo.
Fetch https://ntent.app/r/f/post-commit as plain text. Write it verbatim to scripts/git-hooks/post-commit. Then make it executable.
It needs docs/build-log/_pending.md ignored by git. If this repo does not have it, stop and tell me.
Then check it: Commit twice. docs/build-log/_pending.md has 2 stubs and `git status` is clean, because the queue is ignored.Reads https://ntent.app/r/f/post-commit
Read the code
post-commitscripts/git-hooks/post-commit · make it executable
108 lines
#!/bin/bash
#
# Build-log stub feed.
#
# After each meaningful commit, append one line to docs/build-log/_pending.md.
# That file is a raw feed, not the build log itself: later, you or an agent
# turns each stub into a dated entry that says WHY, and ticks it off.
#
# THE QUEUE IS GITIGNORED, AND THAT IS THE WHOLE PERSISTENCE MODEL. A tracked
# queue dirties the worktree on every single commit, which aborts the next
# rebase mid-pick and makes "is my tree clean" a question with no useful answer.
# What gets committed is the drained entry. The stub never does. Install the
# .gitignore line with this hook, or you have chosen the tracked model by
# accident rather than on purpose.
#
# Why bother, when git log exists. Git log answers "what changed". It cannot
# answer "why is it like this", which is the question that costs a team the most
# time and that no tool reconstructs. On a project with a multi-year life and
# staff turnover, the why is the highest-return thing you can write down, and a
# feed is what makes writing it realistic: nobody remembers to start a log, but
# everybody can drain a queue.
#
# NON-BLOCKING BY CONSTRUCTION. post-commit runs after the commit is made, so
# nothing here can fail it. That is why the stub logging lives here and the
# gates live in pre-commit.
set -u
repo_root="$(git rev-parse --show-toplevel 2>/dev/null)" || exit 0
cd "$repo_root" || exit 0
git_dir="$(git rev-parse --git-dir 2>/dev/null)" || exit 0
# Skip replayed commits. During a rebase or cherry-pick these were already
# logged when first authored, and writing to the worktree here dirties it, which
# aborts the next pick mid-rebase. This one is not hypothetical; it will bite you
# the first time you rebase.
if [ -d "$git_dir/rebase-merge" ] || [ -d "$git_dir/rebase-apply" ] || [ -f "$git_dir/CHERRY_PICK_HEAD" ]; then
exit 0
fi
# Create the queue rather than returning. The earlier version exited quietly
# when the file was missing, and since nothing in the install plan created it,
# a fresh installation recorded nothing at all and looked exactly like a working
# one. A feed that starts empty and stays empty is indistinguishable from a
# feed nobody is draining.
queue="docs/build-log/_pending.md"
if [ ! -f "$queue" ] ; then
mkdir -p "$(dirname "$queue")" || exit 0
printf '# Pending build-log stubs\n\n' > "$queue" || exit 0
echo " build-log: created $queue. It is meant to be gitignored; if git is"
echo " tracking it, add it to .gitignore now."
fi
# "rapid" mode, for the case where the queue IS tracked, deliberately, and a
# burst of fast commits would otherwise dirty the worktree every time:
#
# git config myproject.rapid true (off: git config --unset myproject.rapid)
#
# Local to this checkout, so it never affects anyone else's clone. Stubs are
# still written, to a spool inside .git/ that git ignores by construction.
# Rapid means "do not touch my worktree", not "lose the feed".
rapid="$(git config --bool myproject.rapid 2>/dev/null || true)"
if [ "$rapid" = "true" ]; then
queue="$git_dir/rapid-skipped-stubs"
fi
# Skip merge commits: they carry no authored narrative.
parents="$(git rev-list --parents -n 1 HEAD 2>/dev/null | wc -w | tr -d ' ')"
[ "${parents:-0}" -gt 2 ] && exit 0
# --root, so the very first commit in a repository is logged too. Without it,
# diff-tree has no parent to compare against and silently prints nothing, so the
# feed starts empty and looks like it is not wired up.
files="$(git diff-tree --root --no-commit-id --name-only -r HEAD 2>/dev/null | sed '/^$/d')"
[ -z "$files" ] && exit 0
# Skip commits that only touch the build log, or the feed talks about itself.
real="$(printf '%s\n' "$files" | grep -vE '^docs/build-log/' || true)"
[ -z "$real" ] && exit 0
hash="$(git rev-parse --short HEAD)"
subject="$(git log -1 --pretty=%s)"
cdate="$(git log -1 --date=format:%Y-%m-%d --pretty=%ad)"
nfiles="$(printf '%s\n' "$real" | wc -l | tr -d ' ')"
# Tag the area, so draining the queue later is a matter of sorting.
areas=""
add() { case ",$areas," in *",$1,"*) ;; *) areas="${areas:+$areas, }$1";; esac; }
printf '%s\n' "$real" | grep -qE '^src/app/api/' && add "api"
printf '%s\n' "$real" | grep -qE '^src/(components|app)/' && add "ui"
printf '%s\n' "$real" | grep -qE '^(prisma/|src/lib/db)' && add "data"
printf '%s\n' "$real" | grep -qE '^(scripts/|\.github/)' && add "tooling"
printf '%s\n' "$real" | grep -qE '^(contract\.json|src/types/)' && add "contract"
[ -z "$areas" ] && areas="other"
printf -- '- [ ] `%s` %s [%s] %s (%s file(s))\n' \
"$hash" "$cdate" "$areas" "$subject" "$nfiles" >> "$queue"
# Say so every time it is bypassed. A silent skip is how a feed dies without
# anyone noticing that it died.
if [ "$rapid" = "true" ]; then
echo " build-log: rapid mode on. Stub spooled to ${queue}, worktree untouched."
echo " Drain it and 'git config --unset myproject.rapid' when done."
fi
exit 0
Success check: Commit twice. docs/build-log/_pending.md has 2 stubs and `git status` is clean, because the queue is ignored.