panama update: one command, no questions, and no gap it cannot see
Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
+143
-49
@@ -5,15 +5,22 @@
|
||||
# Author: Gabriel Brown
|
||||
#
|
||||
# Commands:
|
||||
# update Commit & sync local changes (or just pull if clean)
|
||||
# update Bring this machine up to date: pull, then install --upgrade
|
||||
# sync Review, commit & push local changes to this repo
|
||||
# edit Open the Panama repo in Neovim
|
||||
# doctor Report what is actually running on this machine
|
||||
# test Run every contract under tests/
|
||||
# upgrade Re-run the installer from anywhere
|
||||
# upgrade Re-run the installer from anywhere, interview included
|
||||
# migrate Apply repairs this machine has not had yet
|
||||
# apps Choose applications to install, by category
|
||||
# app Build and install an application that no repository packages
|
||||
# help Show this help
|
||||
#
|
||||
# update and sync are deliberately separate verbs. One acts on the machine, the
|
||||
# other on the repository, and a single command that guessed between them by
|
||||
# looking at whether the tree happened to be dirty would do a different job
|
||||
# depending on state nobody can see.
|
||||
#
|
||||
# Designed to grow: add new subcommands as cmd_<name> functions and
|
||||
# register them in the dispatcher / usage block below.
|
||||
|
||||
@@ -64,15 +71,20 @@ ${BOLD}Usage:${RESET}
|
||||
$PROGRAM <command> [options]
|
||||
|
||||
${BOLD}Commands:${RESET}
|
||||
${GREEN}update${RESET} Review, commit & sync local changes. If the working tree is
|
||||
clean it simply runs 'git pull'.
|
||||
${GREEN}update${RESET} Bring this machine up to date. Pulls, then runs the stages
|
||||
that need no questions asked. The routine command; safe to
|
||||
re-run, and it never asks you anything.
|
||||
${GREEN}sync${RESET} Review, commit & push your changes to this repo. Uncommitted
|
||||
work is committed before anything is fetched, so a moved
|
||||
upstream is a rebase rather than a stash conflict.
|
||||
${GREEN}edit${RESET} Open the Panama repo in Neovim.
|
||||
${GREEN}doctor${RESET} Report what is actually running on this machine, rather
|
||||
than what was installed. Takes --summary for one line per check.
|
||||
${GREEN}test${RESET} Run every contract under tests/. Give it a pattern to run
|
||||
a subset: 'panama test dock' runs the ones matching 'dock'.
|
||||
${GREEN}upgrade${RESET} Re-run ./install from anywhere. Safe: every stage is
|
||||
idempotent and this is the documented upgrade path.
|
||||
${GREEN}upgrade${RESET} Re-run ./install from anywhere, interview and all. For a new
|
||||
machine, or to change an answer you gave. Routine updates are
|
||||
'$PROGRAM update', which asks nothing.
|
||||
${GREEN}migrate${RESET} Apply repairs this machine has not had yet. The half of an
|
||||
upgrade that ./install cannot do, because installing only ever
|
||||
adds. Safe to re-run; nothing is applied twice.
|
||||
@@ -89,6 +101,8 @@ ${BOLD}Options:${RESET}
|
||||
|
||||
${BOLD}Examples:${RESET}
|
||||
$PROGRAM update
|
||||
$PROGRAM update --packages
|
||||
$PROGRAM sync
|
||||
$PROGRAM edit
|
||||
$PROGRAM doctor --summary
|
||||
$PROGRAM test dock
|
||||
@@ -112,25 +126,116 @@ require_git_repo() {
|
||||
# ----------------------------------------------------------------------------
|
||||
# Command: update
|
||||
# ----------------------------------------------------------------------------
|
||||
#
|
||||
# The routine command: bring THIS MACHINE up to date. Pull, then hand the rest
|
||||
# to `install --upgrade`, which asks nothing.
|
||||
#
|
||||
# Most of an update needs no stage at all. Every dotfile is a symlink into this
|
||||
# checkout, so an edit to an existing config/dot/** file is live the moment the
|
||||
# pull returns -- there is nothing to apply. Stages earn their place when a pull
|
||||
# brings something structural: a new dotfile directory to link, a file under
|
||||
# config/copy/ to place as root, a new package, a gsettings change. Running the
|
||||
# cheap ones every time is idempotent and takes seconds; working out which were
|
||||
# needed is guesswork with a silent failure mode.
|
||||
#
|
||||
# Uncommitted work never blocks an update. It is stashed across the pull and
|
||||
# restored afterwards -- and if restoring conflicts, the tree is reset rather
|
||||
# than left holding conflict markers, because on this repository those markers
|
||||
# are not something you fix at your leisure. They are live in ~/.config the
|
||||
# instant they are written, and a half-merged .qml is a shell that will not
|
||||
# parse.
|
||||
cmd_update() {
|
||||
require_git_repo
|
||||
cd "$PANAMA_DIR"
|
||||
|
||||
info "Panama repo: ${BOLD}${PANAMA_DIR}${RESET}"
|
||||
|
||||
# Any changes in the working tree? (modified, staged, or untracked)
|
||||
if [[ -z "$(git status --porcelain)" ]]; then
|
||||
info "Working tree is clean — pulling latest changes."
|
||||
if git pull --ff-only; then
|
||||
ok "Already in sync."
|
||||
local stashed=0 conflict_stash=""
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
info "Local changes; stashing them across the pull."
|
||||
if git stash push --include-untracked -m "panama-update-$(date +%s)" >/dev/null; then
|
||||
stashed=1
|
||||
else
|
||||
err "git pull failed."
|
||||
err "Could not stash local changes, so the pull would overwrite them."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --ff-only on purpose. A diverged branch is something to resolve
|
||||
# deliberately, not something an update command should merge on your behalf.
|
||||
# It is a warning rather than an error: the stages below are still worth
|
||||
# running against whatever is checked out.
|
||||
if git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
|
||||
info "Pulling..."
|
||||
if git pull --ff-only; then
|
||||
ok "Checkout is current."
|
||||
else
|
||||
warn "Could not fast-forward — continuing with what is checked out."
|
||||
fi
|
||||
else
|
||||
warn "No upstream configured for this branch; nothing to pull."
|
||||
fi
|
||||
|
||||
if (( stashed )); then
|
||||
if git stash pop >/dev/null 2>&1; then
|
||||
ok "Local changes restored."
|
||||
else
|
||||
# A conflicted pop keeps the stash entry -- git says so itself, and the
|
||||
# contract proves it -- so resetting here loses nothing. The work stays
|
||||
# in the stash, where nothing is reading it, instead of in your live
|
||||
# config as merge markers.
|
||||
git reset --hard HEAD >/dev/null 2>&1
|
||||
conflict_stash="$(git stash list --format='%gd: %gs' 2>/dev/null | head -1)"
|
||||
warn "Your local changes conflict with what was pulled; they stay stashed."
|
||||
fi
|
||||
fi
|
||||
|
||||
local installer="$PANAMA_DIR/install"
|
||||
if [[ ! -x "$installer" ]]; then
|
||||
err "The installer is missing from $installer"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local rc=0
|
||||
"$installer" --upgrade "$@" || rc=$?
|
||||
|
||||
# Repeated at the very end rather than only where it happened. A warning
|
||||
# printed before twenty minutes of dnf output is a warning nobody read.
|
||||
if [[ -n "$conflict_stash" ]]; then
|
||||
echo
|
||||
warn "Your local changes were NOT restored — they conflicted with the pull."
|
||||
printf ' They are safe at %s%s%s\n' "$BOLD" "$conflict_stash" "$RESET"
|
||||
printf ' Restore them with: %sgit stash pop%s\n' "$BOLD" "$RESET"
|
||||
fi
|
||||
|
||||
return $rc
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Command: sync
|
||||
# ----------------------------------------------------------------------------
|
||||
#
|
||||
# The other half of what `update` used to mean: commit and push MY EDITS.
|
||||
# Panama is a working tree people edit in place -- every dotfile is a symlink
|
||||
# into it -- so "I changed something, put it upstream" is a daily action and
|
||||
# deserves its own verb rather than sharing one with "update my machine".
|
||||
#
|
||||
# The commit happens BEFORE anything is fetched, which is why there is no stash
|
||||
# in here. By the time upstream is consulted the work is a commit, so a moved
|
||||
# upstream is a rebase over committed history -- recoverable, ordinary, and
|
||||
# nothing like a stash pop conflicting into a live config.
|
||||
cmd_sync() {
|
||||
require_git_repo
|
||||
cd "$PANAMA_DIR"
|
||||
|
||||
info "Panama repo: ${BOLD}${PANAMA_DIR}${RESET}"
|
||||
|
||||
if [[ -z "$(git status --porcelain)" ]]; then
|
||||
ok "Nothing to commit — the working tree is clean."
|
||||
info "To update this machine, run: ${BOLD}${PROGRAM} update${RESET}"
|
||||
return
|
||||
fi
|
||||
|
||||
# Show what changed
|
||||
header "Changed files"
|
||||
git -c color.status=always status --short
|
||||
|
||||
@@ -154,7 +259,6 @@ cmd_update() {
|
||||
return
|
||||
fi
|
||||
|
||||
# Commit message
|
||||
local msg
|
||||
printf '%s?%s Commit message: ' "${CYAN}${BOLD}" "$RESET"
|
||||
read -r msg || true
|
||||
@@ -163,46 +267,35 @@ cmd_update() {
|
||||
warn "No message given — using: ${BOLD}${msg}${RESET}"
|
||||
fi
|
||||
|
||||
# Is the local branch up to date with its upstream?
|
||||
info "Checking whether the repo is up to date..."
|
||||
if git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
|
||||
git fetch --quiet
|
||||
local local_rev remote_rev base_rev
|
||||
local_rev=$(git rev-parse @)
|
||||
remote_rev=$(git rev-parse '@{u}')
|
||||
base_rev=$(git merge-base @ '@{u}')
|
||||
|
||||
if [[ "$local_rev" == "$remote_rev" ]]; then
|
||||
ok "Repo is up to date."
|
||||
elif [[ "$local_rev" == "$base_rev" ]]; then
|
||||
warn "Repo is behind upstream — stashing, pulling, then re-applying."
|
||||
info "Stashing local changes..."
|
||||
git stash push --include-untracked -m "panama-update-$(date +%s)" >/dev/null
|
||||
|
||||
if ! git pull --ff-only; then
|
||||
err "git pull failed — restoring your changes."
|
||||
git stash pop || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
info "Re-applying stashed changes..."
|
||||
if ! git stash pop; then
|
||||
err "Conflict while re-applying changes. Resolve it, then commit manually."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
warn "Local branch has diverged from upstream — committing locally only."
|
||||
fi
|
||||
else
|
||||
warn "No upstream configured for this branch — committing locally only."
|
||||
fi
|
||||
|
||||
# Commit everything
|
||||
info "Committing changes..."
|
||||
git add -A
|
||||
git commit -m "$msg"
|
||||
ok "Committed: ${BOLD}${msg}${RESET}"
|
||||
|
||||
if ! git rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
|
||||
warn "No upstream configured for this branch — committed locally only."
|
||||
return
|
||||
fi
|
||||
|
||||
# Only now, with the work safely committed, is it worth looking upstream.
|
||||
info "Checking whether upstream has moved..."
|
||||
git fetch --quiet
|
||||
local remote_rev base_rev
|
||||
remote_rev=$(git rev-parse '@{u}')
|
||||
base_rev=$(git merge-base @ '@{u}')
|
||||
|
||||
if [[ "$base_rev" != "$remote_rev" ]]; then
|
||||
warn "Upstream has moved — rebasing your commit onto it."
|
||||
if ! git pull --rebase; then
|
||||
err "The rebase stopped on a conflict."
|
||||
err "Resolve it, then: git rebase --continue"
|
||||
exit 1
|
||||
fi
|
||||
ok "Rebased onto upstream."
|
||||
else
|
||||
ok "Upstream has not moved."
|
||||
fi
|
||||
|
||||
echo
|
||||
if confirm "Push the changes now?"; then
|
||||
info "Pushing..."
|
||||
@@ -539,6 +632,7 @@ main() {
|
||||
local cmd="${1:-}"
|
||||
case "$cmd" in
|
||||
update) shift; cmd_update "$@" ;;
|
||||
sync) shift; cmd_sync "$@" ;;
|
||||
edit) shift; cmd_edit "$@" ;;
|
||||
doctor) shift; cmd_doctor "$@" ;;
|
||||
test) shift; cmd_test "$@" ;;
|
||||
|
||||
Reference in New Issue
Block a user