#!/usr/bin/env bash # dotsync — sync dotfiles with the least churn. # # Deployed configs are symlinks into this repo, so `git pull` makes edits LIVE instantly # with no re-stow — meaning no symlink churn and no app reloads (kitty etc. keep their # runtime state). Re-stowing (install.sh) is only needed when files are ADDED / REMOVED, # to create or prune symlinks. dotsync does the minimum: pull, and re-link ONLY if the # pull added/removed/renamed files. # # dotsync pull, then relink only if the file set changed # dotsync -n preview incoming changes (fetch only; no pull, no relink) # dotsync as above, forcing the host package (normally unnecessary: install.sh # reuses the host recorded in ~/.config/dots-host) set -uo pipefail DOTS="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)" cd "$DOTS" || exit 1 if [ "${1:-}" = "-n" ] || [ "${1:-}" = "--dry-run" ]; then git fetch -q 2>/dev/null || { echo "fetch failed (offline?)" exit 1 } up="$(git rev-parse --abbrev-ref '@{u}' 2>/dev/null)" || { echo "no upstream set" exit 1 } # rev-list, not diff: a plain diff also fires when the LOCAL side is ahead, mislabelling # your own unpushed commits as incoming. Count commits that exist only on the upstream. n="$(git rev-list --count "HEAD..$up")" if [ "$n" -eq 0 ]; then echo "already up to date with $up."; else echo "incoming from $up ($n commit(s)):" git --no-pager diff --stat "HEAD...$up" fi exit 0 fi before="$(git rev-parse HEAD)" if ! git pull --ff-only; then echo "pull not fast-forward (local commits or conflicts) — commit/stash, then retry." >&2 exit 1 fi after="$(git rev-parse HEAD)" if [ "$before" = "$after" ]; then echo "already up to date — nothing to sync." exit 0 fi # Services that read config ONCE at startup don't pick up pulled edits — nudge loudly. # (Symlinked edits are live for everything else; these are the exceptions that bit us.) if git diff --name-only "$before" "$after" | grep -q '\.config/llamacpp/' \ && systemctl cat llama.service &>/dev/null; then echo "" echo "⚠⚠ llamacpp presets changed — the router reads them only at startup. Apply with:" echo " sudo systemctl restart llama.service" echo "" fi # A unit file is read from /etc, not through the stow symlink — needs daemon-reload as well. if git diff --name-only "$before" "$after" | grep -q '\.config/whisper/' \ && systemctl cat whisper.service &>/dev/null; then echo "" echo "⚠⚠ whisper unit changed — apply with:" echo " sudo systemctl daemon-reload && sudo systemctl restart whisper.service" echo "" fi # Modified files (M) are live only through INTACT symlinks — a live file severed by an # atomic rewrite (the recurring claude/settings.json class) swallows pulled edits silently, # so verify that premise per modified file instead of assuming it. Added/Deleted/Renamed # always need (un)linking. The diff is captured once, not piped into `grep -q`: its early # exit can SIGPIPE git under pipefail and turn the whole condition falsely negative. changes="$(git diff --name-status "$before" "$after")" need_restow="" if printf '%s\n' "$changes" | grep -qE '^(A|D|R)'; then need_restow="files added/removed/renamed" else # map each modified repo path (package/.dotpath) to its live counterpart; a real file # (not a symlink) sitting there means the pulled edit did NOT land while IFS= read -r rel; do live="$HOME/$rel" if [ -e "$live" ] && [ ! -L "$live" ]; then echo "note: ~/$rel is a real file, not a symlink into the repo — the pulled edit has NOT landed there." need_restow="severed symlink(s)" fi done < <(printf '%s\n' "$changes" | awk -F'\t' '$1 ~ /^M/ && $2 ~ /^[^\/]+\/\./ { sub(/^[^\/]+\//, "", $2); print $2 }') fi if [ -n "$need_restow" ]; then echo "$need_restow — re-linking via install.sh (this may reload some apps)…" # Normally called with no argument, so install.sh resolves the host from ~/.config/dots-host — # the one recorded by the last install.sh run here. That is exactly why the host must not be # derived from `hostname -s`: this path is automatic and has nothing to pass. An explicit # `dotsync ` is forwarded through for the rare override. exec "$DOTS/install.sh" "$@" else echo "edits only — already live through the symlinks; no re-stow, no reload needed." fi