#!/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 < [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 $? }