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>
134 lines
4.1 KiB
Shell
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 $?
|
|
|
|
}
|