dotfiles/home/dot_local/bin/executable_dotfiles
Felipe M. 99254bf506 Add a dotfiles maintenance command
Wraps the routine operations: `dotfiles update` fetches and fast-forwards,
`dotfiles apply` does that plus `chezmoi apply` and `metapac sync`, and
`dotfiles chezmoi` / `dotfiles metapac` pass through to the underlying tools.

`apply` refuses to run with uncommitted changes in the repo, and `update`
only ever fast-forwards, so a diverged branch is reported rather than merged.
Package removal is left out of `apply` on purpose -- `metapac clean` is
destructive enough to stay an explicit, separate step.

The script body is wrapped in a brace group because `chezmoi apply` can
rewrite the script while it is executing; the braces force bash to parse the
whole file up front rather than reading it incrementally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 12:03:09 +02:00

134 lines
4.1 KiB
Shell

#!/usr/bin/env bash
#
# dotfiles -- maintenance entry point for a chezmoi + metapac machine.
#
# The whole script is wrapped in a brace group on purpose: `dotfiles apply`
# runs `chezmoi apply`, which can rewrite this very file while it is running.
# The braces force bash to parse the entire script up front instead of reading
# it incrementally off disk, so a mid-run rewrite cannot corrupt execution.
{
set -euo pipefail
readonly PROG="${0##*/}"
if [ -t 1 ]; then
B=$'\033[1m'; R=$'\033[31m'; G=$'\033[32m'; Y=$'\033[33m'; N=$'\033[0m'
else
B=''; R=''; G=''; Y=''; N=''
fi
info() { printf '%s==>%s %s\n' "${G}${B}" "$N" "$*"; }
warn() { printf '%s==>%s %s\n' "${Y}${B}" "$N" "$*" >&2; }
die() { printf '%serror:%s %s\n' "${R}${B}" "$N" "$*" >&2; exit 1; }
need() {
command -v "$1" >/dev/null 2>&1 \
|| die "$1 is not installed or not on PATH"
}
# Root of the dotfiles git repo. `chezmoi source-path` points at the
# .chezmoiroot subdirectory (home/), so walk up to the actual repo.
repo_root() {
need chezmoi
local src root
src="$(chezmoi source-path)" \
|| die "could not determine the chezmoi source path"
root="$(git -C "$src" rev-parse --show-toplevel 2>/dev/null)" \
|| die "the chezmoi source at $src is not a git repository"
printf '%s\n' "$root"
}
assert_clean() {
local root=$1
[ -z "$(git -C "$root" status --porcelain)" ] && return 0
printf '\n'
git -C "$root" -c color.status=always status --short
printf '\n'
die "uncommitted changes in $root -- commit or stash them first"
}
cmd_update() {
local root branch upstream counts behind ahead
root="$(repo_root)"
info "fetching $(git -C "$root" remote get-url origin 2>/dev/null || echo origin)"
git -C "$root" fetch --quiet --prune origin
branch="$(git -C "$root" rev-parse --abbrev-ref HEAD)"
upstream="$(git -C "$root" rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)" \
|| die "branch '$branch' has no upstream; set one with: git -C $root push -u origin $branch"
# left = commits only upstream has, right = only we have; tab-separated
counts="$(git -C "$root" rev-list --left-right --count "$upstream...HEAD")"
read -r behind ahead <<<"$counts"
if [ "$behind" -eq 0 ]; then
info "already up to date with $upstream"
else
info "$behind new commit(s) from $upstream:"
git -C "$root" --no-pager log --oneline --no-decorate "HEAD..$upstream"
git -C "$root" merge --ff-only "$upstream" --quiet \
|| die "cannot fast-forward: '$branch' has diverged from $upstream"
info "now at $(git -C "$root" rev-parse --short HEAD)"
fi
[ "$ahead" -gt 0 ] && warn "$ahead local commit(s) not yet pushed to $upstream"
return 0
}
cmd_apply() {
need chezmoi
need metapac
local root
root="$(repo_root)"
assert_clean "$root"
cmd_update
info "applying chezmoi"
chezmoi apply
info "syncing packages with metapac"
metapac sync
info "done"
}
usage() {
cat <<EOF
${B}$PROG${N} -- maintenance for chezmoi + metapac dotfiles
${B}usage:${N} $PROG <command> [args...]
${B}update${N} fetch and fast-forward the dotfiles repo
${B}apply${N} update, apply chezmoi, then install missing packages
(refuses to run with uncommitted changes)
${B}chezmoi${N} [args] run chezmoi
${B}metapac${N} [args] run metapac
${B}help${N} show this message
Removing packages is deliberately not part of '$PROG apply'. Run
'$PROG metapac clean' when you want to prune unmanaged packages.
EOF
}
main() {
case "${1-}" in
update) shift; cmd_update "$@" ;;
apply) shift; cmd_apply "$@" ;;
chezmoi) shift; need chezmoi; exec chezmoi "$@" ;;
metapac) shift; need metapac; exec metapac "$@" ;;
help|-h|--help) usage ;;
'') usage >&2; exit 1 ;;
*) printf '%serror:%s unknown command: %s\n\n' "${R}${B}" "$N" "$1" >&2
usage >&2; exit 1 ;;
esac
}
main "$@"
exit $?
}